iOS中的多任務: Background Fetch,Silent Remote Notifications,Background Transfer Service

WWDC 2013 Session筆記 - iOS7中的多任務

iOS7的後臺多任務特性iOS7的後臺多任務特性

這是我的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後臺的變化在於以下四點:

  • 改變了後臺任務的運行方式
  • 增加了後臺獲取(Background 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和之前的系統中,系統在用戶退出應用後,如果應用正在執行後臺任務的話,系統會保持活躍狀態直到後臺任務完成或者是超時以後,纔會進入真正的低功耗休眠狀態。

iOS6之前的後臺任務處理iOS6之前的後臺任務處理

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

iOS7的後臺任務處理iOS7的後臺任務處理

這個變化在不減少應用的後臺任務時間長度的情況下,給設備帶來了更多的休眠時間,從而延長了續航。對於開發者來說,這個改變更多的是系統層級的變化,對於非網絡傳輸的任務來說,保持原來的用法即可,新系統將會按照新的喚醒方式進行處理;而對於原來在後臺做網絡傳輸的應用來說,蘋果建議在iOS7中使用NSURLSession,創建後臺的session並進行網絡傳輸,這樣可以很容易地利用更好的後臺傳輸API,而不必受限於原來的時長,關於這個具體的我們一會兒再說。

後臺獲取(Background Fetch)

現在的應用無法在後臺獲取信息,比如社交類應用,用戶一定需要在打開應用之後才能進行網絡連接,獲取新的消息條目,然後才能將新內容呈現給用戶。說實話這個體驗並不是很好,用戶在打開應用後必定會有一段時間的等待,每次皆是如此。iOS7中新加入的後臺獲取就是用來解決這個不足的:後臺獲取乾的事情就是在用戶打開應用之前就使app有機會執行代碼來獲取數據,刷新UI。這樣在用戶打開應用的時候,最新的內容將已然呈現在用戶眼前,而省去了所有的加載過程。想想看,沒有加載的網絡體驗的世界,會是怎樣一種感覺。這已經不是smooth,而是真的amazing了。

那具體應該怎麼做呢?一步一步來:

啓用後臺獲取

首先是修改應用的Info.plist,在UIBackgroundModes中加入fetch,即可告訴系統應用需要後臺獲取的權限。另外一種更簡單的方式,得益於Xcode5的Capabilities特性(參見可以參見我之前的一篇WWDC2013筆記 Xcode5和ObjC新特性),現在甚至都不需要去手動修改Info.plist來進行添加了,打開Capabilities頁面下的Background Modes選項,並勾選Background fetch選項即可(如下圖)。

在Capabilities中開啓Background Modes在Capabilities中開啓Background Modes

筆者寫這篇文章的時候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了。

更改Scheme的選項爲從後臺獲取事件中啓動更改Scheme的選項爲從後臺獲取事件中啓動

另一種是當應用在後臺時,模擬一次後臺獲取。這個比較簡單,在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;
一旦後臺傳輸的狀態發生變化(包括正常結束和失敗)的時候,應用將被喚醒並運行appDelegate中的回調,接下來NSURLSessionTask的委託方法將在後臺被調用。雖然上面的例子中直接在appDelegate中call了completionHandler,但是實際上更好的選擇是在appDelegate中暫時持有completionHandler,然後在NSURLSessionTask的delegate方法中檢查是否確實完成了傳輸並更新UI後,再調用completionHandler。另外,你的應用到現在爲止只是在後臺運行,想要提醒用戶傳輸完成的話,也許你還需要在這個時候發送一個本地推送(記住在這個時候你的應用是可以執行代碼的,雖然是在後臺),這樣用戶可以注意到你的應用的變化並回到應用,並開始已經準備好數據和界面。

一些限制

首先,後臺傳輸只會通過wifi來進行,用戶大概也不會開心蜂窩數據的流量被後臺流量用掉。後臺下載的時間與以前的關閉應用後X分鐘的模式不一樣,而是爲了節省電力變爲離散式的下載,並與其他後臺任務併發(比如接收郵件等)。另外還需要注意的是,對於下載後的內容不要忘記寫到應用的目錄下(一般來說這種可以重複獲得的內容應該放到cache目錄下),否則如果由於應用完全退出的情況導致沒有保存到可再次訪問的路徑的話,那可就白做工了。

後臺傳輸非常適合用於文件,照片或者追加遊戲內容關卡等的下載,如果配合後臺獲取或者靜默推送的話,相信可以完全很多很有趣,並且以前被限制而無法實現的功能。


轉自http://onevcat.com/2013/08/ios7-background-multitask/


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章