對(duì)于依賴于實(shí)時(shí)信息、位置服務(wù)或與外部設(shè)備通信的 iOS App ,開(kāi)發(fā)者可以用后臺(tái)刷新來(lái)提高用戶體驗(yàn),允許 App 在后臺(tái)執(zhí)行任務(wù)。特別是在下載或上傳大量數(shù)據(jù)時(shí),后臺(tái)執(zhí)行網(wǎng)絡(luò)請(qǐng)求會(huì)相當(dāng)有幫助。
iOS 限制 App 在后臺(tái)運(yùn)行,也很有道理。如果 App 沒(méi)處于活動(dòng)狀態(tài),就不應(yīng)該使用大量系統(tǒng)資源,尤其是在涉及數(shù)據(jù)傳輸時(shí)。但隨著 App 越來(lái)越多地與后端服務(wù)連接,后臺(tái)獲取數(shù)據(jù)對(duì)于良好的用戶體驗(yàn)已經(jīng)變得更加重要。
不幸的是,并沒(méi)有一種實(shí)現(xiàn)網(wǎng)絡(luò)后臺(tái)請(qǐng)求的最佳方式。最新的 iOS SDK 提供了很多選項(xiàng),熟悉不同的后臺(tái)抓取 API 有助于決定使用哪個(gè)技術(shù)。
由于不受控制的后臺(tái)任務(wù)可能導(dǎo)致設(shè)備的電池壽命大量消耗,并且很難復(fù)現(xiàn),正確使用 iOS 后臺(tái)刷新 API 很關(guān)鍵。本文介紹了相關(guān)問(wèn)題,并且介紹了一些常見(jiàn)的坑。
理解 iOS App 執(zhí)行狀態(tài)
大多數(shù) iOS 用戶都熟悉 iOS 9 中的多任務(wù)界面,雙擊 home 鍵的時(shí)候會(huì)顯示最近使用的 App 列表。向上滑動(dòng)會(huì)強(qiáng)制關(guān)閉它。但是,多任務(wù)界面里顯示的 app 并不一定在執(zhí)行代碼或獲取數(shù)據(jù)。它們可能被暫停或根本沒(méi)有在運(yùn)行(這長(zhǎng)期困擾了想節(jié)省電量的 iOS 用戶)。

使用 Swift, App 的執(zhí)行狀態(tài)可以這么獲得:
UIApplication.sharedApplication().applicationState
如果狀態(tài)是 active,應(yīng)用在屏幕上是可見(jiàn)的,準(zhǔn)備好接收事件。不可見(jiàn)的話可能是 background 或 inactive。蘋果開(kāi)發(fā)者網(wǎng)站上有一張很棒的全狀態(tài)示意圖 。
大多數(shù)開(kāi)發(fā)者使用 UIApplication 里的代理方法或借助大量通知類型來(lái)響應(yīng)狀態(tài)的改變。Xcode 7 的 iOS 模板包含了這些用來(lái)響應(yīng)改變的代理方法:
// App 準(zhǔn)備好運(yùn)行了
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
// App 即將從活躍到非活躍狀態(tài)
func applicationWillResignActive(application: UIApplication)
// 后臺(tái)模式剛被激活
func applicationDidEnterBackground(application: UIApplication)
// App 現(xiàn)在可見(jiàn)了,并可以接收事件
func applicationDidBecomeActive(application: UIApplication)
// App 即將終止
func applicationWillTerminate(application: UIApplication)
默認(rèn)情況下,當(dāng)應(yīng)用進(jìn)入后臺(tái)時(shí),沒(méi)什么值得興奮的——它只是在 app 被暫停之間的短暫過(guò)渡而已。甚至可以禁用后臺(tái)狀態(tài)(但蘋果不鼓勵(lì)這么做 )。盡管后臺(tái)刷新有幾種不同的使用情境,包括和藍(lán)牙設(shè)備的通信、播放音頻等,但許多應(yīng)用使用后臺(tái)刷新來(lái)下載東西。
使用 NSURLSession 在后臺(tái)下載和上傳
當(dāng) App 需要上傳或下載數(shù)據(jù)時(shí),如果用戶發(fā)送短信和切換到其它應(yīng)用,操作最好繼續(xù)。幸運(yùn)的是,當(dāng)應(yīng)用程序變得不活動(dòng)時(shí),NSURLSession 類可以移交下載和上傳到操作系統(tǒng)。與幾乎所有后臺(tái)執(zhí)行 API 一樣,如果用戶從多任務(wù)界面強(qiáng)行退出,后臺(tái)操作會(huì)終止。(注意如果 App 在追蹤位置,用戶強(qiáng)退了,它會(huì)重新啟動(dòng)。)
要使 NSURLSession 具有后臺(tái)能力,需要實(shí)例化有后臺(tái)初始化方法和標(biāo)識(shí)符(重用于所有后臺(tái)會(huì)話)的 NSURLSessionConfiguration 對(duì)象:
let sessionConfig = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("com.newrelic.bgt")
如果一個(gè) App 特別禮貌,在 NSURLSessionConfiguation 中有一個(gè)標(biāo)志稱為“discretionary”,允許 iOS 優(yōu)化性能的請(qǐng)求,因此在某些情況下(如電池電量不足時(shí)不好的連接),請(qǐng)求不會(huì)真正發(fā)生。
backgroundSessionConfig.discretionary = true
只要應(yīng)用程序發(fā)出 HTTP 或 HTTPS 請(qǐng)求,那么 NSURLSession 需要使用配置對(duì)象和委托來(lái)實(shí)例化,以便在下載或上傳完成時(shí)接收通知。 這里 有一些其它限制:
let session = NSURLSession(configuration: backgroundSessionConfig, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
例如為了下載靜態(tài) PDF 文件,具有后臺(tái)配置的會(huì)話可以在標(biāo)準(zhǔn)下載任務(wù)中使用:
let downloadTask = session.downloadTaskWithURL(NSURL(string: "https://try.newrelic.com/rs/newrelic/images/nr_getting_started_guide.pdf")!)
downloadTask.resume()
當(dāng)操作完成或者有錯(cuò)誤時(shí),NSURLSession 委托方法會(huì)被調(diào)用。會(huì)有一個(gè)磁盤上的臨時(shí)文件的路徑,可以打開(kāi)以讀取或移動(dòng)到另一個(gè)位置。
關(guān)于 NSURLSession 的最后一點(diǎn):它從 iOS 9 開(kāi)始支持 HTTP/2。關(guān)于使用API ??的更多細(xì)節(jié)可以在 蘋果的開(kāi)發(fā)者網(wǎng)站 上獲得。
選擇機(jī)會(huì)下載東西
在 iOS 7 里,蘋果添加了對(duì)后臺(tái)抓取的支持——智能、每個(gè) App 都有機(jī)會(huì)被喚醒。沒(méi)有辦法強(qiáng)制后臺(tái)抓取在指定的時(shí)間執(zhí)行。iOS 在調(diào)度未來(lái)的執(zhí)行時(shí)會(huì)檢查早之前的后臺(tái)抓取中使用的數(shù)據(jù)和電池用量。
添加支持要編輯應(yīng)用程序的 property list(參閱 UIBackgroundModes)并在App 生命周期的早期設(shè)置獲取間隔:
application.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)
當(dāng) iOS 決定開(kāi)始后臺(tái)抓取時(shí),會(huì)調(diào)用此 UIApplicationDelegate 方法:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void)
這個(gè)方法有大約 30 秒時(shí)間將一個(gè) UIBackgroundFetchResult 返回給 completionHandler 函數(shù),然后 App 就會(huì)終止。UIBackgroundFetchResult 的值用于確定何時(shí)再次調(diào)用后臺(tái)抓取委托方法。如果在特定時(shí)間頻繁需要數(shù)據(jù)(例如,清晨的新聞 App),這有助于 iOS 了解何時(shí)執(zhí)行后臺(tái)抓?。?/p>
enum UIBackgroundFetchResult : UInt { case NewData case NoData case Failed}
后臺(tái)抓取也可以由遠(yuǎn)程推送通知觸發(fā),并且具有非常類似的委托方法,帶有相同的 completion handler。
要在 iOS 模擬器中測(cè)試后臺(tái)抓取事件,Xcode 在 Scheme 編輯器中有一個(gè)“Launch due to background fetch event”選項(xiàng),并在 debug menu 下有“Simulate Background Fetch”項(xiàng)。

在 2016 年初,開(kāi)發(fā)者發(fā)現(xiàn)是用 iOS 模擬器測(cè)試后臺(tái)抓取會(huì)有問(wèn)題,所以最好是 clean 之后把 App 安裝在真機(jī)上。
此外,Xcode 調(diào)試器改變了操作系統(tǒng)掛起應(yīng)用程序的方式,并且還可能在 測(cè)試非活動(dòng)狀態(tài)時(shí)出現(xiàn)問(wèn)題 。在沒(méi)有連接調(diào)試器的設(shè)備上進(jìn)行測(cè)試(正如用戶和 App 交互一樣)有時(shí)是唯一可靠地再現(xiàn)某些狀態(tài)的方法。
App 終止的特殊情況
用戶在多任務(wù)界面強(qiáng)退可能是出現(xiàn)不可復(fù)現(xiàn)的崩潰的根源。如果 App 被殺死且沒(méi)有任何通知的話就可能發(fā)生。例如,App 被掛起了,但系統(tǒng)由于內(nèi)存不足而終止了它,就不會(huì)發(fā)送任何通知。只有 iOS 想要終止未暫停并處于后臺(tái)狀態(tài)的 App 時(shí),才會(huì)調(diào)用 applicationWillTerminate。

在 iOS 9 中,App 不應(yīng)該依賴于 applicationWillTerminate: 的調(diào)用。最好在 applicationDidEnterBackground: 中保存狀態(tài)并執(zhí)行清理。
然而,重要的是 applicationWillTerminate 被調(diào)用的時(shí)候清理和終止所有正在運(yùn)行的后臺(tái)任務(wù),因?yàn)槿绻?iOS 必須強(qiáng)制殺死正在運(yùn)行的后臺(tái)任務(wù),可能會(huì)導(dǎo)致崩潰。這有時(shí)是難以復(fù)現(xiàn)的 bug 的來(lái)源。
出于性能和電池壽命的考慮,iOS 限制了后臺(tái)的時(shí)間量。在后臺(tái)執(zhí)行狀態(tài)中剩余的時(shí)間量可從以下獲取:
UIApplication.sharedApplication().backgroundTimeRemaining
backgroundTimeRemaining 的數(shù)量并不總是正確。強(qiáng)制退出將停止任何后臺(tái)任務(wù),無(wú)論剩余多少時(shí)間。
關(guān)于執(zhí)行狀態(tài)的代理方法總是被調(diào)用(甚至是按照特定順序)的假設(shè)實(shí)際上也并不一定。仔細(xì)檢查建設(shè)一個(gè)執(zhí)行狀態(tài)總是發(fā)生在另一個(gè)狀態(tài)之前的代碼。
總結(jié)
當(dāng)編寫在后臺(tái)執(zhí)行的 iOS 代碼時(shí):
- 確定要使用哪個(gè)后臺(tái)刷新 API。對(duì)于需要很多秒才能完成的網(wǎng)絡(luò)請(qǐng)求,NSURLSession 會(huì)很有幫助。使用 iOS 提供的機(jī)會(huì)性后臺(tái)抓取代理對(duì)于需要按計(jì)劃獲取內(nèi)容的 app 會(huì)很有幫助。
- 遠(yuǎn)程推送通知可以是觸發(fā)后臺(tái)刷新的有效機(jī)制。
Log 執(zhí)行狀態(tài)的變更,在有和沒(méi)有連接調(diào)試器的真機(jī)上測(cè)試,小心模擬器帶來(lái)的奇怪問(wèn)題。是用開(kāi)源的 iOS logging 庫(kù),例如 CocoaLumberjack 或 XCGLogger 會(huì)很有幫助。 - 訪問(wèn)鑰匙串或使用 iOS 數(shù)據(jù)保護(hù)功能時(shí)要小心。后臺(tái)刷新可能發(fā)生在鎖屏?xí)r,可能導(dǎo)致讀寫受保護(hù)的資源出現(xiàn)問(wèn)題。
- 高性能后臺(tái)代碼很關(guān)鍵:iOS 會(huì)優(yōu)先處理前臺(tái)的 App,嚴(yán)格限制 App 完成后臺(tái)任務(wù)的資源和時(shí)間。
隨著移動(dòng)數(shù)據(jù)使用量的增加和新的 iOS 9 功能(如 iPad 上的多任務(wù)處理拆分視圖),管理應(yīng)用執(zhí)行狀態(tài)對(duì)于構(gòu)建高質(zhì)量應(yīng)用程序非常重要——App 打開(kāi)時(shí)持續(xù)不斷的進(jìn)度指示條肯定會(huì)讓用戶很煩。后臺(tái)刷新是蘋果對(duì)開(kāi)發(fā)人員的妥協(xié),旨在平衡用戶體驗(yàn)與使用數(shù)據(jù)網(wǎng)絡(luò)和高網(wǎng)絡(luò)延遲時(shí)導(dǎo)致的電池消耗。利用后臺(tái)抓取 API 保持信息最新,并注意避免常見(jiàn)的坑,這有助于滿足用戶對(duì) App 始終快速且永不崩潰的期望。