iOS 7系列譯文:iOS7的多任務處理(轉)

本文由 伯樂在線 - ylovesy 翻譯自 David Caunt。歡迎加入技術翻譯小組。轉載請參見文章末尾處的要求。

  在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/
轉載必須在正文中標註並保留原文鏈接、譯文鏈接和譯者等信息。]

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