這是我的WWDC2013系列筆記中的一篇,完整的筆記列表請(qǐng)參看這篇總覽。本文僅作為個(gè)人記錄使用,也歡迎在許可協(xié)議范圍內(nèi)轉(zhuǎn)載或使用,但是還煩請(qǐng)保留原文鏈接,謝謝您的理解合作。如果您覺得本站對(duì)您能有幫助,您可以使用RSS或郵件方式訂閱本站,這樣您將能在第一時(shí)間獲取本站信息。
本文涉及到的WWDC2013 Session有
- Session 204 What's New with Multitasking
- Session 705 What’s New in Foundation Networking
iOS7以前的Multitasking
iOS的多任務(wù)是在iOS4的時(shí)候被引入的,在此之前iOS的app都是按下Home鍵就被干掉了。iOS4雖然引入了后臺(tái)和多任務(wù),但是實(shí)際上是偽多任務(wù),一般的app后臺(tái)并不能執(zhí)行自己的代碼,只有少數(shù)幾類服務(wù)在通過注冊(cè)后可以真正在后臺(tái)運(yùn)行,并且在提交到AppStore的時(shí)候也會(huì)被嚴(yán)格審核是否有越權(quán)行為,這種限制主要是出于對(duì)于設(shè)備的續(xù)航和安全兩方面進(jìn)行的考慮。之后經(jīng)過iOS5和6的逐漸發(fā)展,后臺(tái)能運(yùn)行的服務(wù)的種類雖然出現(xiàn)了增加,但是iOS后臺(tái)的本質(zhì)并沒有變化。在iOS7之前,系統(tǒng)所接受的應(yīng)用多任務(wù)可以大致分為幾種:
- 后臺(tái)完成某些花費(fèi)時(shí)間的特定任務(wù)
- 后臺(tái)播放音樂等
- 位置服務(wù)
- IP電話(VoIP)
- Newsstand
在WWDC 2013的keynote上,iOS7的后臺(tái)多任務(wù)改進(jìn)被專門拿出來向開發(fā)者進(jìn)行了介紹,到底iOS7里多任務(wù)方面有什么新的特性可以利用,如何使用呢?簡(jiǎn)單來說,iOS7在后臺(tái)特性方面有很大改進(jìn),不僅改變了以往的一些后臺(tái)任務(wù)處理方式,還加入了全新的后臺(tái)模式,本文將針對(duì)iOS7中新的后臺(tái)特性進(jìn)行一些學(xué)習(xí)和記錄。大體來說,iOS7后臺(tái)的變化在于以下四點(diǎn):
- 改變了后臺(tái)任務(wù)的運(yùn)行方式
- 增加了后臺(tái)獲取(Background Fetch)
- 增加了推送喚醒(靜默推送,Silent Remote Notifications)
- 增加了后臺(tái)傳輸(?Background Transfer Service)
iOS7的多任務(wù)
后臺(tái)任務(wù)
首先看看后臺(tái)任務(wù)的變化,先說這方面的改變,而不是直接介紹新的API,是因?yàn)檫@個(gè)改變很典型地代表了iOS7在后臺(tái)任務(wù)管理和能耗控制上的大體思路。從上古時(shí)期開始(其實(shí)也就4.0),UIApplication提供了-beginBackgroundTaskWithExpirationHandler:
方法來使app在被切到后臺(tái)后仍然能保持運(yùn)行一段時(shí)間,app可以用這個(gè)方法來確保一些很重很慢的工作可以在急不可耐的用戶將你的應(yīng)用扔到后臺(tái)后還能完成,比如編碼視頻,上傳下載某些重要文件或者是完成某些數(shù)據(jù)庫(kù)操作等,雖然時(shí)間不長(zhǎng),但在大多數(shù)情況下勉強(qiáng)夠用。如果你之前沒有使用過這個(gè)API的話,它使用起來大概是長(zhǎng)這個(gè)樣子的:
- (void) doUpdate dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self beginBackgroundUpdateTask]; NSURLResponse * response = nil; NSError * error = nil; NSData * responseData = [NSURLConnection sendSynchronousRequest: request returningResponse: &response error: &error]; // Do something with the result [self endBackgroundUpdateTask]; }); } - (void) beginBackgroundUpdateTask { self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ [self endBackgroundUpdateTask]; }]; } - (void) endBackgroundUpdateTask { [[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask]; self.backgroundUpdateTask = UIBackgroundTaskInvalid; }
在beginBackgroundTaskWithExpirationHandler:
里寫一個(gè)超時(shí)處理(系統(tǒng)只給app分配了一定時(shí)間來進(jìn)行后臺(tái)任務(wù),超時(shí)之前會(huì)調(diào)用這個(gè)block),然后進(jìn)行開始進(jìn)行后臺(tái)任務(wù)處理,在任務(wù)結(jié)束或者過期的時(shí)候call一下endBackgroundTask:
使之與begin方法配對(duì)(否則你的app在后臺(tái)任務(wù)超時(shí)的時(shí)候會(huì)被殺掉)。同時(shí),你可以使用UIApplication實(shí)例的backgroundTimeRemaining屬性來獲取剩余的后臺(tái)執(zhí)行時(shí)間。
具體的執(zhí)行時(shí)間來說,在iOS6和之前的系統(tǒng)中,系統(tǒng)在用戶退出應(yīng)用后,如果應(yīng)用正在執(zhí)行后臺(tái)任務(wù)的話,系統(tǒng)會(huì)保持活躍狀態(tài)直到后臺(tái)任務(wù)完成或者是超時(shí)以后,才會(huì)進(jìn)入真正的低功耗休眠狀態(tài)。

而在iOS7中,后臺(tái)任務(wù)的處理方式發(fā)生了改變。系統(tǒng)將在用戶鎖屏后盡快讓設(shè)備進(jìn)入休眠狀態(tài),以節(jié)省電力,這時(shí)后臺(tái)任務(wù)是被暫停的。之后在設(shè)備在特定時(shí)間進(jìn)行系統(tǒng)應(yīng)用的操作被喚醒(比如檢查郵件或者接到來電等)時(shí),之前暫停的后臺(tái)任務(wù)將一起進(jìn)行。就是說,系統(tǒng)不會(huì)專門為第三方的應(yīng)用保持設(shè)備處于活動(dòng)狀態(tài)。如下圖示

這個(gè)變化在不減少應(yīng)用的后臺(tái)任務(wù)時(shí)間長(zhǎng)度的情況下,給設(shè)備帶來了更多的休眠時(shí)間,從而延長(zhǎng)了續(xù)航。對(duì)于開發(fā)者來說,這個(gè)改變更多的是系統(tǒng)層級(jí)的變化,對(duì)于非網(wǎng)絡(luò)傳輸?shù)娜蝿?wù)來說,保持原來的用法即可,新系統(tǒng)將會(huì)按照新的喚醒方式進(jìn)行處理;而對(duì)于原來在后臺(tái)做網(wǎng)絡(luò)傳輸?shù)膽?yīng)用來說,蘋果建議在iOS7中使用NSURLSession
,創(chuàng)建后臺(tái)的session并進(jìn)行網(wǎng)絡(luò)傳輸,這樣可以很容易地利用更好的后臺(tái)傳輸API,而不必受限于原來的時(shí)長(zhǎng),關(guān)于這個(gè)具體的我們一會(huì)兒再說。
后臺(tái)獲取(Background Fetch)
現(xiàn)在的應(yīng)用無法在后臺(tái)獲取信息,比如社交類應(yīng)用,用戶一定需要在打開應(yīng)用之后才能進(jìn)行網(wǎng)絡(luò)連接,獲取新的消息條目,然后才能將新內(nèi)容呈現(xiàn)給用戶。說實(shí)話這個(gè)體驗(yàn)并不是很好,用戶在打開應(yīng)用后必定會(huì)有一段時(shí)間的等待,每次皆是如此。iOS7中新加入的后臺(tái)獲取就是用來解決這個(gè)不足的:后臺(tái)獲取干的事情就是在用戶打開應(yīng)用之前就使app有機(jī)會(huì)執(zhí)行代碼來獲取數(shù)據(jù),刷新UI。這樣在用戶打開應(yīng)用的時(shí)候,最新的內(nèi)容將已然呈現(xiàn)在用戶眼前,而省去了所有的加載過程。想想看,沒有加載的網(wǎng)絡(luò)體驗(yàn)的世界,會(huì)是怎樣一種感覺。這已經(jīng)不是smooth,而是真的amazing了。
那具體應(yīng)該怎么做呢?一步一步來:
啟用后臺(tái)獲取
首先是修改應(yīng)用的Info.plist,在UIBackgroundModes
中加入fetch,即可告訴系統(tǒng)應(yīng)用需要后臺(tái)獲取的權(quán)限。另外一種更簡(jiǎn)單的方式,得益于Xcode5的Capabilities特性(參見可以參見我之前的一篇WWDC2013筆記 Xcode5和ObjC新特性),現(xiàn)在甚至都不需要去手動(dòng)修改Info.plist來進(jìn)行添加了,打開Capabilities頁(yè)面下的Background Modes選項(xiàng),并勾選Background fetch選項(xiàng)即可(如下圖)。

筆者寫這篇文章的時(shí)候iOS7還沒有上市,也沒有相關(guān)的審核資料,所以不知道如果只是在這里打開了fetch選項(xiàng),但卻沒有實(shí)現(xiàn)的話,應(yīng)用會(huì)不會(huì)無法通過審核。但是依照蘋果一貫的做法來看,如果聲明了需要某項(xiàng)后臺(tái)權(quán)限,但是結(jié)果卻沒有相關(guān)實(shí)現(xiàn)的話,被拒掉的可能性還是比較大的。因此大家盡量不要拿上線產(chǎn)品進(jìn)行實(shí)驗(yàn),而應(yīng)當(dāng)是在demo項(xiàng)目里研究明白以后再到上線產(chǎn)品中進(jìn)行實(shí)裝。
設(shè)定獲取間隔
對(duì)應(yīng)用的UIApplication實(shí)例設(shè)置獲取間隔,一般在應(yīng)用啟動(dòng)的時(shí)候調(diào)用以下代碼即可:
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
如果不對(duì)最小后臺(tái)獲取間隔進(jìn)行設(shè)定的話,系統(tǒng)將使用默認(rèn)值UIApplicationBackgroundFetchIntervalNever
,也就是永遠(yuǎn)不進(jìn)行后臺(tái)獲取。當(dāng)然,-setMinimumBackgroundFetchInterval:
方法接受的是NSTimeInterval,因此你也可以手動(dòng)指定一個(gè)以秒為單位的最小獲取間隔。需要注意的是,我們都已經(jīng)知道iOS是一個(gè)非常霸道為我獨(dú)尊的系統(tǒng),因此自然也不可能讓一介區(qū)區(qū)第三方應(yīng)用來控制系統(tǒng)行為。這里所指定的時(shí)間間隔只是代表了“在上一次獲取或者關(guān)閉應(yīng)用之后,在這一段時(shí)間內(nèi)一定不會(huì)去做后臺(tái)獲取”,而真正具體到什么時(shí)候會(huì)進(jìn)行后臺(tái)獲取,那~完全是要看系統(tǒng)娘的心情的~我們是無從得知的。系統(tǒng)將根據(jù)你的設(shè)定,選擇比如接收郵件的時(shí)候順便為你的應(yīng)用獲取一下,或者也有可能專門為你的應(yīng)用喚醒一下設(shè)備。作為開發(fā)者,我們應(yīng)該做的是為用戶的電池考慮,盡可能地選擇合適自己應(yīng)用的后臺(tái)獲取間隔。設(shè)置為UIApplicationBackgroundFetchIntervalMinimum的話,系統(tǒng)會(huì)盡可能多盡可能快地為你的應(yīng)用進(jìn)行后臺(tái)獲取,但是比如對(duì)于一個(gè)天氣應(yīng)用,可能對(duì)實(shí)時(shí)的數(shù)據(jù)并不會(huì)那么關(guān)心,就完全不必設(shè)置為UIApplicationBackgroundFetchIntervalMinimum,也許1小時(shí)會(huì)是一個(gè)更好的選擇。新的Mac OSX 10.9上已經(jīng)出現(xiàn)了功耗監(jiān)測(cè),用于讓用戶確定什么應(yīng)用是能耗大戶,有理由相信同樣的東西也可能出現(xiàn)在iOS上。如果不想讓用戶因?yàn)槟愕膽?yīng)用是耗電大戶而怒刪的話,從現(xiàn)在開始注意一下應(yīng)用的能耗還是蠻有必要的(做綠色環(huán)保低碳的iOS app,從今天開始~)。
實(shí)現(xiàn)后臺(tái)獲取代碼并通知系統(tǒng)
在完成了前兩步后,只需要在AppDelegate里實(shí)現(xiàn)-application:performFetchWithCompletionHandler:
就行了。系統(tǒng)將會(huì)在執(zhí)行fetch的時(shí)候調(diào)用這個(gè)方法,然后開發(fā)者需要做的是在這個(gè)方法里完成獲取的工作,然后刷新UI,并通知系統(tǒng)獲取結(jié)束,以便系統(tǒng)盡快回到休眠狀態(tài)。獲取數(shù)據(jù)這是應(yīng)用相關(guān)的內(nèi)容,在此不做贅述,應(yīng)用在前臺(tái)能完成的工作在這里都能做,唯一的限制是系統(tǒng)不會(huì)給你很長(zhǎng)時(shí)間來做fetch,一般會(huì)小于一分鐘,而且fetch在絕大多數(shù)情況下將和別的應(yīng)用共用網(wǎng)絡(luò)連接。這些時(shí)間對(duì)于fetch一些簡(jiǎn)單數(shù)據(jù)來說是足夠的了,比如微博的新條目(大圖除外),接下來一小時(shí)的天氣情況等。如果涉及到較大文件的傳輸?shù)脑挘煤笈_(tái)獲取的API就不合適了,而應(yīng)該使用另一個(gè)新的文件傳輸?shù)腁PI,我們稍后再說。類似前面提到的后臺(tái)任務(wù)完成時(shí)必須通知系統(tǒng)一樣,在在獲取完成后,也必須通知系統(tǒng)獲取完成,方法是調(diào)用-application:performFetchWithCompletionHandler:
的handler。這個(gè)CompletionHandler接收一個(gè)UIBackgroundFetchResult
作為參數(shù),可供選擇的結(jié)果有UIBackgroundFetchResultNewData
,UIBackgroundFetchResultNoData
,UIBackgroundFetchResultFailed
三種,分別表示獲取到了新數(shù)據(jù)(此時(shí)系統(tǒng)將對(duì)現(xiàn)在的UI狀態(tài)截圖并更新App Switcher中你的應(yīng)用的截屏),沒有新數(shù)據(jù),以及獲取失敗。寫一個(gè)簡(jiǎn)單的例子吧:
//File: YourAppDelegate.m -(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { UINavigationController *navigationController = (UINavigationController*)self.window.rootViewController; id fetchViewController = navigationController.topViewController; if ([fetchViewController respondsToSelector:@selector(fetchDataResult:)]) { [fetchViewController fetchDataResult:^(NSError *error, NSArray *results){ if (!error) { if (results.count != 0) { //Update UI with results. //Tell system all done. completionHandler(UIBackgroundFetchResultNewData); } else { completionHandler(UIBackgroundFetchResultNoData); } } else { completionHandler(UIBackgroundFetchResultFailed); } }]; } else { completionHandler(UIBackgroundFetchResultFailed); } }
當(dāng)然,實(shí)際情況中會(huì)比這要復(fù)雜得多,用戶當(dāng)前的ViewController是否合適做獲取,獲取后的數(shù)據(jù)如何處理都需要考慮。另外要說明的是上面的代碼在獲取成功后直接在appDelegate里更新UI,這只是為了能在同一處進(jìn)行說明,但卻是不正確的結(jié)構(gòu)。比較好的做法是將獲取和更新UI的業(yè)務(wù)邏輯都放到fetchViewController里,然后向其發(fā)送獲取消息的時(shí)候?qū)ompletionHandler作為參數(shù)傳入,并在fetchViewController里完成獲取結(jié)束的報(bào)告。
另一個(gè)比較神奇的地方是系統(tǒng)將追蹤用戶的使用習(xí)慣,并根據(jù)對(duì)每個(gè)應(yīng)用的使用時(shí)刻給一個(gè)合理的fetch時(shí)間。比如系統(tǒng)將記錄你在每天早上9點(diǎn)上班的電車上,中午12點(diǎn)半吃飯時(shí),以及22點(diǎn)睡覺前會(huì)刷一下微博,只要這個(gè)習(xí)慣持續(xù)個(gè)三四天,系統(tǒng)便會(huì)將應(yīng)用的后臺(tái)獲取時(shí)刻調(diào)節(jié)為9點(diǎn),12點(diǎn)和22點(diǎn)前一點(diǎn)。這樣在每次你打開應(yīng)用都直接有最新內(nèi)容的同時(shí),也節(jié)省了電量和流量。
后臺(tái)獲取的調(diào)試
既然是系統(tǒng)決定的fetch,那我們要如何測(cè)試寫的代碼呢?難道是將應(yīng)用退到后臺(tái),然后安心等待系統(tǒng)進(jìn)行后臺(tái)獲取么?當(dāng)然不是...Xcode5為我們提供了兩種方法來測(cè)試后臺(tái)獲取的代碼。一種是從后臺(tái)獲取中啟動(dòng)應(yīng)用,另一種是當(dāng)應(yīng)用在后臺(tái)時(shí)模擬一次后臺(tái)推送。
對(duì)于前者,我們可以新建一個(gè)Scheme來專門調(diào)試從后臺(tái)啟動(dòng)。點(diǎn)擊Xcode5的Product->Scheme->Edit Scheme(或者直接使用快捷鍵⌘<
)。在編輯Scheme的窗口中點(diǎn)Duplicate Scheme按鈕復(fù)制一個(gè)當(dāng)前方案,然后在新Scheme的option中將Background Fetch打上勾。從這個(gè)Scheme來運(yùn)行應(yīng)用的時(shí)候,應(yīng)用將不會(huì)直接啟動(dòng)切入前臺(tái),而是調(diào)用后臺(tái)獲取部分代碼并更新UI,這樣再點(diǎn)擊圖標(biāo)進(jìn)入應(yīng)用時(shí),你應(yīng)該可以看到最新的數(shù)據(jù)和更新好的UI了。

另一種是當(dāng)應(yīng)用在后臺(tái)時(shí),模擬一次后臺(tái)獲取。這個(gè)比較簡(jiǎn)單,在app調(diào)試運(yùn)行時(shí),點(diǎn)擊Xcode5的Debug菜單中的Simulate Background Fetch,即可模擬完成一次獲取調(diào)用。
推送喚醒(Remote Notifications)
遠(yuǎn)程推送(??Remote Push Notifications)可以說是增加用戶留存率的不二法則,在iOS6和之前,推送的類型是很單一的,無非就是顯示標(biāo)題內(nèi)容,指定聲音等。用戶通過解鎖進(jìn)入你的應(yīng)用后,appDelegate中通過推送打開應(yīng)用的回調(diào)將被調(diào)用,然后你再獲取數(shù)據(jù),進(jìn)行顯示。這和沒有后臺(tái)獲取時(shí)的打開應(yīng)用后再獲取數(shù)據(jù)刷新的問題是一樣的。在iOS7中這個(gè)行為發(fā)生了一些改變,我們有機(jī)會(huì)使設(shè)備在接收到遠(yuǎn)端推送后讓系統(tǒng)喚醒設(shè)備和我們的后臺(tái)應(yīng)用,并先執(zhí)行一段代碼來準(zhǔn)備數(shù)據(jù)和UI,然后再提示用戶有推送。這時(shí)用戶如果解鎖設(shè)備進(jìn)入應(yīng)用后將不會(huì)再有任何加載過程,新的內(nèi)容將直接得到呈現(xiàn)。
實(shí)裝的方法和剛才的后臺(tái)獲取比較類似,還是一步步來:
啟用推送喚醒
和上面的后臺(tái)獲取類似,更改Info.plist,在UIBackgroundModes
下加入remote-notification
即可開啟,當(dāng)然同樣的更簡(jiǎn)單直接的辦法是使用Capabilities。
更改推送的payload
在iOS7中,如果想要使用推送來喚醒應(yīng)用運(yùn)行代碼的話,需要在payload中加入content-available
,并設(shè)置為1。
aps { content-available: 1 alert: {...} }
??
實(shí)現(xiàn)推送喚醒代碼并通知系統(tǒng)
最后在appDelegate中實(shí)現(xiàn)?-application:didReceiveRemoteNotification:fetchCompletionHandle:
。這部分內(nèi)容和上面的后臺(tái)獲取部分完全一樣,在此不再重復(fù)。
一些限制和應(yīng)用的例子
因?yàn)橐坏┩扑统晒Γ脩舻脑O(shè)備將被喚醒,因此這類推送不可能不受到限制。Apple將限制此類推送的頻率,當(dāng)頻率超過一定限制后,帶有content-available標(biāo)志的推送將會(huì)被阻塞,以保證用戶設(shè)備不被頻繁喚醒。按照Apple的說法,這個(gè)頻率在一小時(shí)內(nèi)個(gè)位數(shù)次的推送的話不會(huì)有太大問題。
Apple給出了幾個(gè)典型的應(yīng)用情景,比如一個(gè)電視節(jié)目類的應(yīng)用,當(dāng)用戶標(biāo)記某些劇目為喜愛時(shí),當(dāng)這些劇有更新時(shí),可以給用戶發(fā)送靜默的喚醒推送通知,客戶端在接到通知后檢查更新并開始后臺(tái)下載(注意后臺(tái)下載的部分絕對(duì)不應(yīng)該在推送回調(diào)中做,而是應(yīng)該使用新的后臺(tái)傳輸服務(wù),后面詳細(xì)介紹)。下載完成后發(fā)送一個(gè)本地推送告知用戶新的內(nèi)容已經(jīng)準(zhǔn)備完畢。這樣在用戶注意到推送并打開應(yīng)用的時(shí)候,所有必要的內(nèi)容已經(jīng)下載完畢,UI也將切換至用戶喜愛的劇目,用戶只需要點(diǎn)擊播放即可開始真正使用應(yīng)用,這絕對(duì)是無比順暢和優(yōu)秀的體驗(yàn)。另一種應(yīng)用情景是文件同步類,比如用戶標(biāo)記了一些文件為需要隨時(shí)同步,這樣用戶在其他設(shè)備或網(wǎng)頁(yè)服務(wù)上更改了這些文件時(shí),可以發(fā)送靜默推送然后使用后臺(tái)傳輸來保持這些文件隨時(shí)是最新。
如果您是一路看下來的話,不難發(fā)現(xiàn)其實(shí)后臺(tái)獲取和靜默推送在很多方面是很類似的,特別是實(shí)現(xiàn)和處理的方式,但是它們適用的情景是完全不同的。后臺(tái)獲取更多地使用在泛數(shù)據(jù)模式下,也即用戶對(duì)特定數(shù)據(jù)并不是很關(guān)心,數(shù)據(jù)應(yīng)該被更新的時(shí)間也不是很確定,典型的有社交類應(yīng)用和天氣類應(yīng)用;而靜默推送或者是推送喚醒更多地應(yīng)該是用戶感興趣的內(nèi)容發(fā)生更新時(shí)被使用,比如消息類應(yīng)用和內(nèi)容型服務(wù)等。根據(jù)不同的應(yīng)用情景,選擇合適的后臺(tái)策略(或者混合使用兩者),以帶給用戶絕佳體驗(yàn),這是Apple所期望iOS7開發(fā)者做到的。
后臺(tái)傳輸(?Background Transfer Service)
iOS6和之前,iOS應(yīng)用在大塊數(shù)據(jù)的下載這一塊限制是比較多的:只有應(yīng)用在前臺(tái)時(shí)能保持下載(用戶按Home鍵切到后臺(tái)或者是等到設(shè)備自動(dòng)休眠都可能中止下載),在后臺(tái)只有很短的最多十分鐘時(shí)間可以保持網(wǎng)絡(luò)連接。如果想要完成一個(gè)較大數(shù)據(jù)的下載,用戶將不得不打開你的app并且基本無所事事。很多這種時(shí)候,用戶會(huì)想要是在下載的時(shí)候能切到別的應(yīng)用刷刷微博或者玩玩游戲,然后再切回來的就已經(jīng)下載完成了的話,該有多好。iOS7中,這可以實(shí)現(xiàn)了。iOS7引入了后臺(tái)傳輸?shù)南嚓P(guān)方式,用來保證應(yīng)用退出后數(shù)據(jù)下載或者上傳能繼續(xù)進(jìn)行。這種傳輸是由iOS系統(tǒng)進(jìn)行管理的,沒有時(shí)間限制,也不要求應(yīng)用運(yùn)行在前臺(tái)。
想要實(shí)現(xiàn)后臺(tái)傳輸,就必須使用iOS7的新的網(wǎng)絡(luò)連接的類,NSURLSession。這是iOS7中引入用以替代陳舊的NSURLConnection的類,著名的AFNetworking甚至不惜從底層開始完全重寫以適配iOS7和NSURLSession(參見這里),NSURLSession的重要性可見一斑。在這里我主要只介紹NSURLSession在后臺(tái)傳輸中的一些使用,關(guān)于這個(gè)類的其他用法和對(duì)原有NSURLConnection的加強(qiáng),只做稍微帶過而不展開,有興趣深入挖掘和使用的童鞋可以參看Apple的文檔(或者更簡(jiǎn)單的方式是使用AFNetworking來處理網(wǎng)絡(luò)相關(guān)內(nèi)容,而不是直接和CFNetwork框架打交道)。
步驟和例子
后臺(tái)傳輸?shù)牡膶?shí)現(xiàn)也十分簡(jiǎn)單,簡(jiǎn)單說分為三個(gè)步驟:創(chuàng)建后臺(tái)傳輸用的NSURLSession對(duì)象;向這個(gè)對(duì)象中加入對(duì)應(yīng)的傳輸?shù)腘SURLSessionTask,并開始傳輸;在實(shí)現(xiàn)appDelegate里實(shí)現(xiàn)-application:handleEventsForBackgroundURLSession:completionHandler:
方法,以刷新UI及通知系統(tǒng)傳輸結(jié)束。接下來結(jié)合代碼來看一看實(shí)際的用法吧~
首先我們需要一個(gè)用于后臺(tái)下載的session:
- (NSURLSession *)backgroundSession { //Use dispatch_once_t to create only one background session. If you want more than one session, do with different identifier static NSURLSession *session = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.yourcompany.appId.BackgroundSession"]; session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; }); return session; }
這里創(chuàng)建并配置了NSURLSession,將其指定為后臺(tái)session并設(shè)定delegate。
接下來向其中加入對(duì)應(yīng)的傳輸用的NSURLSessionTask,并啟動(dòng)下載。
//@property (nonatomic) NSURLSession *session; //@property (nonatomic) NSURLSessionDownloadTask *downloadTask; - (NSURLSession *)backgroundSession { //... } - (void) beginDownload { NSURL *downloadURL = [NSURL URLWithString:DownloadURLString]; NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL]; self.session = [self backgroundSession]; self.downloadTask = [self.session downloadTaskWithRequest:request]; [self.downloadTask resume]; }
最后一步是在appDelegate中實(shí)現(xiàn)-application:handleEventsForBackgroundURLSession:completionHandler:
//AppDelegate.m - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler { //Check if all transfers are done, and update UI //Then tell system background transfer over, so it can take new snapshot to show in App Switcher completionHandler(); //You can also pop up a local notification to remind the user //... }
NSURLSession和對(duì)應(yīng)的NSURLSessionTask有以下重要的delegate方法可以使用:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location; - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;
一旦后臺(tái)傳輸?shù)臓顟B(tài)發(fā)生變化(包括正常結(jié)束和失敗)的時(shí)候,應(yīng)用將被喚醒并運(yùn)行appDelegate中的回調(diào),接下來NSURLSessionTask的委托方法將在后臺(tái)被調(diào)用。雖然上面的例子中直接在appDelegate中call了completionHandler,但是實(shí)際上更好的選擇是在appDelegate中暫時(shí)持有completionHandler,然后在NSURLSessionTask的delegate方法中檢查是否確實(shí)完成了傳輸并更新UI后,再調(diào)用completionHandler。另外,你的應(yīng)用到現(xiàn)在為止只是在后臺(tái)運(yùn)行,想要提醒用戶傳輸完成的話,也許你還需要在這個(gè)時(shí)候發(fā)送一個(gè)本地推送(記住在這個(gè)時(shí)候你的應(yīng)用是可以執(zhí)行代碼的,雖然是在后臺(tái)),這樣用戶可以注意到你的應(yīng)用的變化并回到應(yīng)用,并開始已經(jīng)準(zhǔn)備好數(shù)據(jù)和界面。
一些限制
首先,后臺(tái)傳輸只會(huì)通過wifi來進(jìn)行,用戶大概也不會(huì)開心蜂窩數(shù)據(jù)的流量被后臺(tái)流量用掉。后臺(tái)下載的時(shí)間與以前的關(guān)閉應(yīng)用后X分鐘的模式不一樣,而是為了節(jié)省電力變?yōu)殡x散式的下載,并與其他后臺(tái)任務(wù)并發(fā)(比如接收郵件等)。另外還需要注意的是,對(duì)于下載后的內(nèi)容不要忘記寫到應(yīng)用的目錄下(一般來說這種可以重復(fù)獲得的內(nèi)容應(yīng)該放到cache目錄下),否則如果由于應(yīng)用完全退出的情況導(dǎo)致沒有保存到可再次訪問的路徑的話,那可就白做工了。
后臺(tái)傳輸非常適合用于文件,照片或者追加游戲內(nèi)容關(guān)卡等的下載,如果配合后臺(tái)獲取或者靜默推送的話,相信可以完全很多很有趣,并且以前被限制而無法實(shí)現(xiàn)的功能。