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

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

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

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

另一種是當應用在后臺時,模擬一次后臺獲取。這個比較簡單,在app調試運行時,點擊Xcode5的Debug菜單中的Simulate Background Fetch,即可模擬完成一次獲取調用。
推送喚醒(Remote Notifications)
遠程推送(??Remote Push Notifications)可以說是增加用戶留存率的不二法則,在iOS6和之前,推送的類型是很單一的,無非就是顯示標題內容,指定聲音等。用戶通過解鎖進入你的應用后,appDelegate中通過推送打開應用的回調將被調用,然后你再獲取數據,進行顯示。這和沒有后臺獲取時的打開應用后再獲取數據刷新的問題是一樣的。在iOS7中這個行為發生了一些改變,我們有機會使設備在接收到遠端推送后讓系統喚醒設備和我們的后臺應用,并先執行一段代碼來準備數據和UI,然后再提示用戶有推送。這時用戶如果解鎖設備進入應用后將不會再有任何加載過程,新的內容將直接得到呈現。
實裝的方法和剛才的后臺獲取比較類似,還是一步步來:
啟用推送喚醒
和上面的后臺獲取類似,更改Info.plist,在UIBackgroundModes
下加入remote-notification
即可開啟,當然同樣的更簡單直接的辦法是使用Capabilities。
更改推送的payload
在iOS7中,如果想要使用推送來喚醒應用運行代碼的話,需要在payload中加入content-available
,并設置為1。
aps { content-available: 1 alert: {...} }
??
實現推送喚醒代碼并通知系統
最后在appDelegate中實現?-application:didReceiveRemoteNotification:fetchCompletionHandle:
。這部分內容和上面的后臺獲取部分完全一樣,在此不再重復。
一些限制和應用的例子
因為一旦推送成功,用戶的設備將被喚醒,因此這類推送不可能不受到限制。Apple將限制此類推送的頻率,當頻率超過一定限制后,帶有content-available標志的推送將會被阻塞,以保證用戶設備不被頻繁喚醒。按照Apple的說法,這個頻率在一小時內個位數次的推送的話不會有太大問題。
Apple給出了幾個典型的應用情景,比如一個電視節目類的應用,當用戶標記某些劇目為喜愛時,當這些劇有更新時,可以給用戶發送靜默的喚醒推送通知,客戶端在接到通知后檢查更新并開始后臺下載(注意后臺下載的部分絕對不應該在推送回調中做,而是應該使用新的后臺傳輸服務,后面詳細介紹)。下載完成后發送一個本地推送告知用戶新的內容已經準備完畢。這樣在用戶注意到推送并打開應用的時候,所有必要的內容已經下載完畢,UI也將切換至用戶喜愛的劇目,用戶只需要點擊播放即可開始真正使用應用,這絕對是無比順暢和優秀的體驗。另一種應用情景是文件同步類,比如用戶標記了一些文件為需要隨時同步,這樣用戶在其他設備或網頁服務上更改了這些文件時,可以發送靜默推送然后使用后臺傳輸來保持這些文件隨時是最新。
如果您是一路看下來的話,不難發現其實后臺獲取和靜默推送在很多方面是很類似的,特別是實現和處理的方式,但是它們適用的情景是完全不同的。后臺獲取更多地使用在泛數據模式下,也即用戶對特定數據并不是很關心,數據應該被更新的時間也不是很確定,典型的有社交類應用和天氣類應用;而靜默推送或者是推送喚醒更多地應該是用戶感興趣的內容發生更新時被使用,比如消息類應用和內容型服務等。根據不同的應用情景,選擇合適的后臺策略(或者混合使用兩者),以帶給用戶絕佳體驗,這是Apple所期望iOS7開發者做到的。
后臺傳輸(?Background Transfer Service)
iOS6和之前,iOS應用在大塊數據的下載這一塊限制是比較多的:只有應用在前臺時能保持下載(用戶按Home鍵切到后臺或者是等到設備自動休眠都可能中止下載),在后臺只有很短的最多十分鐘時間可以保持網絡連接。如果想要完成一個較大數據的下載,用戶將不得不打開你的app并且基本無所事事。很多這種時候,用戶會想要是在下載的時候能切到別的應用刷刷微博或者玩玩游戲,然后再切回來的就已經下載完成了的話,該有多好。iOS7中,這可以實現了。iOS7引入了后臺傳輸的相關方式,用來保證應用退出后數據下載或者上傳能繼續進行。這種傳輸是由iOS系統進行管理的,沒有時間限制,也不要求應用運行在前臺。
想要實現后臺傳輸,就必須使用iOS7的新的網絡連接的類,NSURLSession。這是iOS7中引入用以替代陳舊的NSURLConnection的類,著名的AFNetworking甚至不惜從底層開始完全重寫以適配iOS7和NSURLSession(參見這里),NSURLSession的重要性可見一斑。在這里我主要只介紹NSURLSession在后臺傳輸中的一些使用,關于這個類的其他用法和對原有NSURLConnection的加強,只做稍微帶過而不展開,有興趣深入挖掘和使用的童鞋可以參看Apple的文檔(或者更簡單的方式是使用AFNetworking來處理網絡相關內容,而不是直接和CFNetwork框架打交道)。
步驟和例子
后臺傳輸的的實現也十分簡單,簡單說分為三個步驟:創建后臺傳輸用的NSURLSession對象;向這個對象中加入對應的傳輸的NSURLSessionTask,并開始傳輸;在實現appDelegate里實現-application:handleEventsForBackgroundURLSession:completionHandler:
方法,以刷新UI及通知系統傳輸結束。接下來結合代碼來看一看實際的用法吧~
首先我們需要一個用于后臺下載的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; }
這里創建并配置了NSURLSession,將其指定為后臺session并設定delegate。
接下來向其中加入對應的傳輸用的NSURLSessionTask,并啟動下載。
//@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中實現-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和對應的NSURLSessionTask有以下重要的delegate方法可以使用:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location; - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;
一旦后臺傳輸的狀態發生變化(包括正常結束和失?。┑臅r候,應用將被喚醒并運行appDelegate中的回調,接下來NSURLSessionTask的委托方法將在后臺被調用。雖然上面的例子中直接在appDelegate中call了completionHandler,但是實際上更好的選擇是在appDelegate中暫時持有completionHandler,然后在NSURLSessionTask的delegate方法中檢查是否確實完成了傳輸并更新UI后,再調用completionHandler。另外,你的應用到現在為止只是在后臺運行,想要提醒用戶傳輸完成的話,也許你還需要在這個時候發送一個本地推送(記住在這個時候你的應用是可以執行代碼的,雖然是在后臺),這樣用戶可以注意到你的應用的變化并回到應用,并開始已經準備好數據和界面。
一些限制
首先,后臺傳輸只會通過wifi來進行,用戶大概也不會開心蜂窩數據的流量被后臺流量用掉。后臺下載的時間與以前的關閉應用后X分鐘的模式不一樣,而是為了節省電力變為離散式的下載,并與其他后臺任務并發(比如接收郵件等)。另外還需要注意的是,對于下載后的內容不要忘記寫到應用的目錄下(一般來說這種可以重復獲得的內容應該放到cache目錄下),否則如果由于應用完全退出的情況導致沒有保存到可再次訪問的路徑的話,那可就白做工了。
后臺傳輸非常適合用于文件,照片或者追加游戲內容關卡等的下載,如果配合后臺獲取或者靜默推送的話,相信可以完全很多很有趣,并且以前被限制而無法實現的功能。