在iOS7之前,當程序退出後,開發者對程序幾乎做不了什麼。除了VOIP和基於位置的特性,唯一能夠在後臺運行代碼的途徑只有使用後臺任務(background tasks),但後臺任務只會執行幾分鐘。如果你想要下載一部很大的視頻以便離線觀看,或者將用戶圖片備份到服務器,你只能完成部分的任務。
ios7新添加了兩個可以在後臺更新應用程序界面和內容的APIs。第一個API是後臺獲取(Background Fetch),允許你在定期間隔內從網絡獲取新內容。第二個API是遠程通知 (Remote Notification),它是一個新特性,它在當新事件發生時利用推送通知(Push Notifications)去告知程序。這兩個新的機制,幫助你保持程序界面最新,還可以在新的後臺傳輸服務(Background Transfer Service)中安排任務,這允許你在進程外執行網絡傳輸(下載和上傳)。
後臺獲取(Background Fetch)和遠程通知(Remote Notification)基於簡單的應用程序委託鉤子,在應用程序掛起之前的30秒時鐘時間開始執行工作。它們不是用於CPU頻繁工作或者長時間運行任務,而是用來處理長時間運行的網絡請求隊列,例如下載一部很大的電影,或者執行快速的內容更新。
在用戶看來,多任務處理唯一明顯的變化就是新的程序切換器(app switcher),它會顯示當程序退出前臺時每一個程序的界面快照。顯示這些快照是有原因的:當完成後臺工作時,開發者可以更新程序快照,顯示新內容的預覽。社交網絡,新聞,或者天氣的應用程序,可以在用戶不打開應用程序的情況下顯示最新的內容。接下來我們會展示怎麼樣更新快照。
後臺獲取(Background Fetch)
後臺獲取(Background Fetch)是一種智能的輪詢機制,它很適合需要經常更新內容的程序,像社交網絡,新聞或天氣的程序。爲了在用戶啓動程序前提前觸發後臺獲取,系統會根據用戶行爲喚醒應用程序。舉個例子,如果用戶經常在下午1點使用某個應用程序,系統會學習,適應並在使用週期前執行後臺獲取。爲了減少電池使用,後臺獲取(Background Fetch)會跨應用程序被設備的無線電合併,如果你向系統報告新數據無法獲取,iOS會適應並使用此信息避免會繼續獲取。
開啓後臺獲取的第一步是在info plist文件中的UIBackgroundModes健值指定使用的特性。最簡單的途徑是在Xcode5的project editor中新的性能標籤頁中(Capabilities tab)設置,這個標籤頁包含了後臺模式部分,可以方便配置多任務選項。
或者,你可以手動編輯這個健值:
<key>UIBackgroundModes</key> <array> <string>fetch</string> </array>
下一步,告訴iOS你希望多久進行一次後臺獲取:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum]; return YES; }
iOS默認不進行後臺獲取,所以你需要設置一個時間間隔,否則,你的應用程序永遠不行在後臺進行獲取數據。UIApplicationBackgroundFetchIntervalMinimum這個值要求系統儘可能經常去管理應用程序什麼時候會被喚醒,但如果不需要這個值,你應該指定你的時間間隔。例如,一個天氣的應用程序,可能只需要幾個小時才更新一次,iOS將會在後臺獲取之間至少等待你指定的時間間隔。
如果你的應用允許用戶退出登錄,那麼就沒有獲取新數據的需要了,你應該把minimumBackgroundFetchInterval設置爲UIApplicationBackgroundFetchIntervalNever,這樣可以節省資源。
最後一步是在應用程序委託中實現下列方法:
- (void) application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; NSURL *url = [[NSURL alloc] initWithString:@"http://yourserver.com/data.json"]; NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (error) { completionHandler(UIBackgroundFetchResultFailed); return; } // Parse response/data and determine whether new content was available BOOL hasNewData = ... if (hasNewData) { completionHandler(UIBackgroundFetchResultNewData); } else { completionHandler(UIBackgroundFetchResultNoData); } }]; // Start the task [task resume]; }
系統喚醒應用程序後將會執行這個委託方法。需要注意的是,你只有30秒的時間來確定獲取的新內容是否可用,然後處理新內容並更新界面。30秒時間應該足夠去從網絡獲取數據和獲取界面的縮略圖,最多隻有30秒。當完成了網絡請求和更新界面後,你應該調用完成的處理代碼。
完成的處理代碼有兩個目的。首先,系統會估量你的進程消耗的電量,並根據你傳遞的UIBackgroundFetchResult 參數記錄新數據是否可用。其次,當你調用完成的處理代碼時,應用的界面縮略圖會被採用,並更新應用程序切換器。當用戶在應用間切換時,用戶將會看到新內容。這種快照行爲的完成代碼,在新的多任務處理APIs中,很很常見的。
在實際應用中,你應該將completionHandler 傳遞到應用程序的子組件,然後在處理完數據和更新界面後調用。
在這裏,你可能想知道iOS是如何在應用程序後臺運行時獲得界面快照的,並且想知道應用程序的生命週期與後臺獲取之間有什麼關係。如果應用程序處於掛起狀態,系統會先喚醒應用,然後再調用application: performFetchWithCompletionHandler:。如果應用程序還沒有啓動,系統將會啓動它,然後調用常見的委託方法,包括application: didFinishLaunchingWithOptions:。你可以把這種應用程序運行的方式想像爲用戶從Springboard啓動這個程序,區別僅僅在於界面是看不見的,在屏幕外渲染的。
大多數情況下,無論應用在後臺啓動或者在前臺,你會執行相同的工作,但你可以通過查看UIApplication的applicationState屬性來判斷應用是不是從後臺啓動。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSLog(@"Launched in background %d", UIApplicationStateBackground == application.applicationState); return YES; }
測試後臺獲取(Testing Background Fetch)
有兩種可以模擬後臺獲取的途徑。最簡單是從Xcode運行你的應用,當應用運行時,在Xcode的Debug菜單選擇Simulate Background Fetch.
第二種方法,使用scheme更改Xcode運行程序的方式。在Xcode菜單的Product選項,選擇Scheme然後選擇Manage Schemes.在這裏,你可以編輯或者添加一個新的scheme,然後選中Launch due to a background fetch event。如下圖:
遠程通知(Remote Notifications)
遠程通知允許你在重要事件發生時,告知你的應用。你可能需要發送新的即時信息,突發新聞的提醒,或者用戶喜愛電視的最新劇集已經可以下載以便離線觀看的消息。遠程通知很適合偶爾出現,但當前很重要的內容,這在後臺獲取之間出現的延遲是不允許的。遠程通知會比後臺獲取更有效率,因爲應用程序只有在需要的時候纔會啓動。
一條遠程通知實際上只是一條普通的帶有content-available標誌的推送通知。當你在後臺更新界面時,你可以發送一條帶有提醒信息的推送去告訴用戶。但遠程通知可以做到在安靜地,沒有提醒消息或者任何聲音的情況下,只去更新應用界面或者觸發後臺工作。然後你可以在完成下載或者處理完新內容後,發送一條本地通知。
靜默的推送通知有速度限制,所以你可以勇敢地根據應用程序的需要發送通知。iOS和蘋果推送服務會控制推送通知多久被遞送,發送很多推送通知是沒有問題的。如果你的推送通知被禁止,推送通知可能會被延遲,直到設備下次發送保持活動狀態的數據包,或者收到另外一個通知。
發送遠程通知(Sending Remote Notifications)
要發送一條遠程通知,需要在推送通知的有效負載(payload)設置content-available標誌。content-available標誌和用來通知Newsstand應用的健值是一樣的,因此,大多數推送腳本和庫都已經支持遠程通知。當你發送一條遠程通知時,你可能還想要包含一些通知有效負載(payload)中的數據,讓你應用程序可以引用時間。這可以爲你節省一些網絡請求,並提高應用程序的響應度。
我建議在開發的時候,使用Nomad CLI’s Houston工具發送推送消息,你也可以使用你喜歡的庫或腳本。
你可以通過nomad-cli ruby gem安裝Houston:
gem install nomad-cli
然後通過包含在Nomad的apn實用工具發送一條通知:
# Send a Push Notification to your Device apn push <device token> -c /path/to/key-cert.pem -n -d content-id=42
在這裏,-n標誌指定應該包含content-available健值,-d標誌允許添加我們自定義的數據健值到有效負荷(payload)。
通知的有效負荷(payload)結果和下面類似:
{ "aps" : { "content-available" : 1 }, "content-id" : 42 }
iOS7添加了一個新的應用程序委託方法,當接收到一條帶有content-available的推送通知時,這個方法被調用:
- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { NSLog(@"Remote Notification userInfo is %@", userInfo); NSNumber *contentID = userInfo[@"content-id"]; // Do something with the content ID completionHandler(UIBackgroundFetchResultNewData); }
然後,應用程序進入後臺啓動,有30秒的時間去獲取新內容並更新界面,最後調用完成的處理代碼。我們可以像後臺獲取那樣,執行快速的網絡請求,但我們可以使用新的強大的後臺傳輸服務,處理任務隊列,下面看看我們如何在任務完成後更新界面。
NSURLSession and Background Transfer Service
NSURLSession是iOS7添加的一個新類,它也是Foundation networking中的新技術。作爲NSURLConnection的替代品,一些熟悉的概念和類都保留下來了,例如NSURL,NSURLRequest和NSURLRespond。所以,你可以使用NSURLConnection的替代品——NSURLSessionTask,處理網絡請求及響應。一共有3中會話任務:數據,下載和上傳。每一種都向NSURLSessionTask添加了語法糖(syntactic sugar),根據你的需要,適當選擇一種。
一個NSURLSession對象協調一個或多個NSURLSessionTask對象,並根據NSURLSessionTask創建的NSURLSessionConfiguration實現不同的功能。使用相同的配置,你也可以創建多組具有相關任務的NSURLSession對象。要利用後臺傳輸服務,你將會使用[NSURLSessionConfiguration backgroundSessionConfiguration]來創建一個會話配置。添加到後臺會話的任務在外部進程運行,即使應用程序被掛起,崩潰,或者被殺死,依然會運行。
NSURLSessionConfiguration允許你設置默認的HTTP頭部,配置緩存策略,限制使用蜂窩數據等等。其中一個選項是discretionary標誌,這個標誌允許系統爲分配任務進行性能優化。這意味着只有當設備有足夠電量時,設備才通過Wifi進行數據傳輸。如果電量低,或者只僅有一個蜂窩連接,傳輸任務是不會運行的。後臺傳輸總是在discretionary模式下運行。
目前爲止,我們大概瞭解了NSURLSession,以及一個後臺會話如何進行,接下來,讓我們回到遠程通知的例子,添加一些代碼來處理後臺傳輸服務的下載隊列。當下載完成後,我們會通知用戶該文件已經可以使用了。
NSURLSessionDownloadTask
首先,我們先處理一條遠程通知,並把一個NSURLSessionDownloadTask添加到後臺傳輸服務的隊列。在backgroundURLSession方法中,我們根據後臺會話配置,創建一個NSURLSession對象,並把應用程序委託對象(application delegate)作爲會話的委託對象。文檔反對對於相同的標識符(identifier)創建多個會話對象,所以我們使用dispatch_once來避免潛在的問題。
- (NSURLSession *)backgroundURLSession { static NSURLSession *session = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSString *identifier = @"io.objc.backgroundTransferExample"; NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfiguration:identifier]; session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:[NSOperationQueue mainQueue]]; }); return session; } - (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { NSLog(@"Received remote notification with userInfo %@", userInfo); NSNumber *contentID = userInfo[@"content-id"]; NSString *downloadURLString = [NSString stringWithFormat:@"http://yourserver.com/downloads/%d.mp3", [contentID intValue]]; NSURL* downloadURL = [NSURL URLWithString:downloadURLString]; NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL]; NSURLSessionDownloadTask *task = [[self backgroundURLSession] downloadTaskWithRequest:request]; task.taskDescription = [NSString stringWithFormat:@"Podcast Episode %d", [contentID intValue]]; [task resume]; completionHandler(UIBackgroundFetchResultNewData); }
我們使用NSURLSession類方法創建一個下載任務,配置請求,並提供說明供以後使用。因爲所有會話任務一開始處於掛起狀態,你必須謹記要調用[task resume]保證開始了任務。
現在,我們需要實現NSURLSessionDownloadDelegate的委託方法,當下載完成時,調用回調函數。如果你需要處理認證或會話生命週期的其他事件,你可能還需要實現NSURLSessionDelegate或NSURLSessionTaskDelegate的方法。你應該閱讀Apple的Life Cycle of a URL Session with Custom Delegates文檔,它講解了所有類型的會話任務的完整生命週期。
NSURLSessionDownloadDelegate中的委託方法全部是必須實現的,儘管在這個例子中我們只需要用到[NSURLSession downloadTask:didFinishDownloadingToURL:]。任務完成下載時,你會得到一個磁盤上該文件的臨時URL。你必須把這個文件移動或複製你的應用程序空間,因爲當你從這個委託方法返回時,該文件將從臨時存儲中刪除。
#Pragma Mark - NSURLSessionDownloadDelegate - (void) URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { NSLog(@"downloadTask:%@ didFinishDownloadingToURL:%@", downloadTask.taskDescription, location); // Copy file to your app's storage with NSFileManager // ... // Notify your UI } - (void) URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { } - (void) URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { }
當後臺會話任務完成時,如果你的應用程序仍然在前臺運行,上面的代碼已經足夠了。然而,在大多數情況下,你的應用程序沒有運行,或者在後臺被掛起。在這些情況下,你必須實現應用程序委託的兩個方法,這樣系統就可以喚醒你的應用程序。不同於以往的委託回調,該應用程序委託會被調用兩次,因爲您的會話和任務委託可能會收到一系列消息。應用程序委託的:handleEventsForBackgroundURLSession:方法,在這些NSURLSession委託的消息發送前被調用,然後,URLSessionDidFinishEventsForBackgroundURLSession被調用。在前面的方法中,儲存了一個後臺完成處理代碼(completionHandler),並在後面的方法中調用該代碼更新界面。
- (void) application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler { // You must re-establish a reference to the background session, // or NSURLSessionDownloadDelegate and NSURLSessionDelegate methods will not be called // as no delegate is attached to the session. See backgroundURLSession above. NSURLSession *backgroundSession = [self backgroundURLSession]; NSLog(@"Rejoining session with identifier %@ %@", identifier, backgroundSession); // Store the completion handler to update your UI after processing session events [self addCompletionHandler:completionHandler forSession:identifier]; } - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { NSLog(@"Background URL session %@ finished events.\n", session); if (session.configuration.identifier) { // Call the handler we stored in -application:handleEventsForBackgroundURLSession: [self callCompletionHandlerForSession:session.configuration.identifier]; } } - (void)addCompletionHandler:(CompletionHandlerType)handler forSession:(NSString *)identifier { if ([self.completionHandlerDictionary objectForKey:identifier]) { NSLog(@"Error: Got multiple handlers for a single session identifier. This should not happen.\n"); } [self.completionHandlerDictionary setObject:handler forKey:identifier]; } - (void)callCompletionHandlerForSession: (NSString *)identifier { CompletionHandlerType handler = [self.completionHandlerDictionary objectForKey: identifier]; if (handler) { [self.completionHandlerDictionary removeObjectForKey: identifier]; NSLog(@"Calling completion handler for session %@", identifier); handler(); } }
如果當後臺傳輸完成時,應用程序不再在前臺,那麼,對於更新程序界面來說,這兩步是必要的。此外,如果當後臺傳輸完成時,應用程序根本沒有在運行,iOS將會在後臺啓動該應用程序,然後前面的應用程序和會話的委託方法會在application:didFinishLaunchingWithOptions:.方法被調用之後被調用。
配置和限制(Configuration and Limitation)
我們簡單地體驗了後臺傳輸的強大之處,但你應該深入文檔,閱讀NSURLSessionConfiguration部分,以便最好地滿足你的情況。例如,NSURLSessionTasks通過NSURLSessionConfiguration的timeoutIntervalForResource屬性,支持資源超時特性。你可以使用這個特性指定你允許完成一個傳輸所需的最長時間。內容只在有限的時間可用,或者在用戶只有有限Wifi帶寬的時間內無法下載或上傳資源的情況下,你也可以使用這個特性。
除了下載任務,NSURLSession也全面支持上傳任務,因此,你可能會在後臺將視頻上傳到服務器,這保證用戶不需要再像iOS6那樣離開正在運行的應用程序。如果當傳輸完成時你的應用程序不需要在後臺運行,一個比較好的做法是,把NSURLSessionConfiguration的sessionSendsLaunchEvents屬性設置爲NO。高效利用系統資源,是一件讓iOS和用戶都高興的事。
最後,我們來說一說使用後臺會話的幾個限制。作爲一個必須實現的委託,您不能對NSURLSession使用簡單的基於塊的回調方法。後臺啓動應用程序,是相對耗費較多資源的,所以總是採用HTTP重定向。後臺傳輸服務只支持HTTP和HTTPS,你不能使用自定義的協議。系統會根據可用的資源進行優化,在任何時候你都不能強制傳輸任務在後臺進行。
另外,要注意,在後臺會話中,NSURLSessionDataTasks 是完全不支持的,你應該只出於短期的,小請求爲目的使用這些任務,而不是用來下載或上傳。
總結
iOS7中新添加的多任務處理和網絡的APIs十分強大,它們爲現有和新的應用程序開闢了一系列可能。如果你的應用程序可以從進程外的網絡傳輸和數據中獲益,那麼盡情地使用這些美妙的APIs!一般情況下,實現後臺傳輸,可以假裝你的應用程序正在前臺運行,並進行適當的界面更新,而這大部分的工作已經爲你完成了。
- 使用適當的新API,爲你的應用程序提供內容服務。
- 儘可能早地有效率調用完成處理代碼。
- 讓完成的處理代碼爲應用程序更新界面快照。
擴展閱讀
- WWDC 2013 session “What’s New with Multitasking”
- WWDC 2013 session “What’s New in Foundation Networking”
- URL Loading System Programming Guide
原文鏈接: David Caunt 翻譯: 伯樂在線 - ylovesy
譯文鏈接: http://blog.jobbole.com/51660/
[ 轉載必須在正文中標註並保留原文鏈接、譯文鏈接和譯者等信息。]