iOS 7 多任務和后臺下載:蘋果全新的多任務API允許用戶在后臺智能的更新和下載內容。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)設置,這個標簽頁包含了后臺模式部分,可以方便配置多任務選項。
或者,你可以手動編輯這個健值(去plist里面添加):
下一步,告訴iOS你希望多久進行一次后臺獲取:
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
BOOLhasNewData = ...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屬性來判斷應用是不是從后臺啟動。
遠程通知(Remote Notifications)
遠程通知允許你在重要事件發生時,告知你的應用。你可能需要發送新的即時信息,突發新聞的提醒,或者用戶喜愛電視的最新劇集已經可以下載以便離線觀看的消息。遠程通知很適合偶爾出現,但當前很重要的內容,這在后臺獲取之間出現的延遲是不允許的。遠程通知會比后臺獲取更有效率,因為應用程序只有在需要的時候才會啟動。
一條遠程通知實際上只是一條普通的帶有content-available標志的推送通知。當你在后臺更新界面時,你可以發送一條帶有提醒信息的推送去告訴用戶。但遠程通知可以做到在安靜地,沒有提醒消息或者任何聲音的情況下,只去更新應用界面或者觸發后臺工作。然后你可以在完成下載或者處理完新內容后,發送一條本地通知。
靜默的推送通知有速度限制,所以你可以勇敢地根據應用程序的需要發送通知。iOS和蘋果推送服務會控制推送通知多久被遞送,發送很多推送通知是沒有問題的。如果你的推送通知被禁止,推送通知可能會被延遲,直到設備下次發送保持活動狀態的數據包,或者收到另外一個通知。
發送遠程通知(Sending Remote Notifications)
要發送一條遠程通知,需要在推送通知的有效負載(payload)設置content-available標志。content-available標志和用來通知Newsstand應用的健值是一樣的,因此,大多數推送腳本和庫都已經支持遠程通知。當你發送一條遠程通知時,你可能還想要包含一些通知有效負載(payload)中的數據,讓你應用程序可以引用時間。這可以為你節省一些網絡請求,并提高應用程序的響應度。
我建議在開發的時候,使用Nomad CLI’s Houston工具發送推送消息,你也可以使用你喜歡的庫或腳本。
OS7添加了一個新的應用程序委托方法,當接收到一條帶有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(@"Receivedremote 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:@"PodcastEpisode %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*)downloadTaskdidFinishDownloadingToURL:(NSURL*)location
{
NSLog(@"downloadTask:%@didFinishDownloadingToURL:%@",downloadTask.taskDescription, location);
//Copy file to your app's storage with NSFileManager
//
...
//
Notify your UI
}
-(void)URLSession:(NSURLSession *)sessiondownloadTask:(NSURLSessionDownloadTask*)downloadTaskdidResumeAtOffset:(int64_t)fileOffsetexpectedTotalBytes:(int64_t)expectedTotalBytes
{
}
-(void)URLSession:(NSURLSession *)sessiondownloadTask:(NSURLSessionDownloadTask*)downloadTaskdidWriteData:(int64_t)bytesWrittentotalBytesWritten:(int64_t)totalBytesWrittentotalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
}
當后臺會話任務完成時,如果你的應用程序仍然在前臺運行,上面的代碼已經足夠了。然而,在大多數情況下,你的應用程序沒有運行,或者在后臺被掛起。在這些情況下,你必須實現應用程序委托的兩個方法,這樣系統就可以喚醒你的應用程序。不同于以往的委托回調,該應用程序委托會被調用兩次,因為您的會話和任務委托可能會收到一系列消息。應用程序委托的:handleEventsForBackgroundURLSession:方法,在這些NSURLSession委托的消息發送前被調用,然后,URLSessionDidFinishEventsForBackgroundURLSession被調用。在前面的方法中,儲存了一個后臺完成處理代碼(completionHandler),并在后面的方法中調用該代碼更新界面。
-(void)application:(UIApplication *)applicationhandleEventsForBackgroundURLSession:(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(@"Rejoiningsession with identifier %@ %@",identifier, backgroundSession);
//Store the completion handler to update your UI after processing session events
[selfaddCompletionHandler:completionHandler forSession:identifier];
}
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession*)session
{
NSLog(@"BackgroundURL session %@ finished events.\n",session);
if(session.configuration.identifier) {
//Call the handler we stored in -application:handleEventsForBackgroundURLSession:
[selfcallCompletionHandlerForSession:session.configuration.identifier];
}
}
-(void)addCompletionHandler:(CompletionHandlerType)handlerforSession:(NSString*)identifier
{
if([self.completionHandlerDictionary objectForKey:identifier]) {
NSLog(@"Error:Got multiple handlers for a single session identifier.? This should not happen.\n");
}
[self.completionHandlerDictionarysetObject:handler forKey:identifier];
}
-(void)callCompletionHandlerForSession:(NSString *)identifier
{
CompletionHandlerType handler = [self.completionHandlerDictionary objectForKey: identifier];
if(handler) {
[self.completionHandlerDictionaryremoveObjectForKey: identifier];
NSLog(@"Callingcompletion 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,為你的應用程序提供內容服務。
盡可能早地有效率調用完成處理代碼。
讓完成的處理代碼為應用程序更新界面快照。