iOS 7學習:多任務處理之Background Fetch

來源:互聯網
上載者:User

在iOS7中,Apple官方為開發人員提供了兩個可以在後台更新應用程式介面和內容的API。第一個API是後台擷取(Background Fetch),該API允許開發人員在一個周期間隔後進行特定的動作,如擷取網路內容、更新程式介面等等。第二個API是遠程通知 (Remote Notification),它是一個新特性,它在當新事件發生時利用推播通知(Push Notifications)去告知程式。這兩個新特性都是在後台進行的,這樣更加有利於多任務執行。

本文只講後台抓取內容(Background Fetch)。(在發送遠程推送的時候貌似需要認證方面,比較複雜,所以這裡沒有嘗試第二項內容)

多任務的一個顯著表現就是背景app switcher介面(這個在iOS 6越獄外掛程式中就玩過了),該介面會顯示出所有背景程式在退出前台時的一個介面快照。當完成後台工作時,開發人員可以更新程式快照,顯示新內容的預覽。例如開啟背景微博我們可以看到badgeNumber提示、qq的資訊提示、最新天氣情況提示等等。這樣使得使用者在不開啟應用程式的情況下預覽最新的內容。後台抓取內容(Background Fetch)非常適用於完成上面的任務。

 

下面來看個Demo。

第一步,為程式配置後台模式:

 

第二步,設定程式的Background Fetch的時間周期:

 

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{    [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];        return YES;}

這裡的BackgroundFetchInterval可以設定兩個值:

 

 

UIKIT_EXTERN const NSTimeInterval UIApplicationBackgroundFetchIntervalMinimum NS_AVAILABLE_IOS(7_0);UIKIT_EXTERN const NSTimeInterval UIApplicationBackgroundFetchIntervalNever NS_AVAILABLE_IOS(7_0);

其中UIApplicationBackgroundFetchIntervalMinimum表示系統應該儘可能經常去管理程式什麼時候被喚醒並執行fetch任務,如果是UIApplicationBackgroundFetchIntervalNever那麼我們的程式將永遠不能在後台擷取程式,當然如果我們的程式完成某個任務並且不再需要後台載入資料時應該使用該值關閉Background Fetch功能。

 

如果這兩個值都不需要,也可以在這裡自行設定一個NSTimeInterval值。

 

接著是實現非常關鍵的委託方法:

 

/// Applications with the fetch background mode may be given opportunities to fetch updated content in the background or when it is convenient for the system. This method will be called in these situations. You should call the fetchCompletionHandler as soon as you're finished performing that operation, so the system can accurately estimate its power and data cost.- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler NS_AVAILABLE_IOS(7_0);

 

系統喚醒背景應用程式後將會執行這個委託方法。需要注意的是,你只有30秒的時間來確定擷取的新內容是否可用(在objc.io的iOS 7 Multitasking一文中指出:後台擷取(Background Fetch)和遠程通知(Remote Notification)在應用程式喚醒之前的30秒時間開始執行工作),然後處理新內容並更新介面。30秒時間應該足夠去從網路擷取資料和擷取介面的縮圖,最多隻有30秒。其中參數completionHandler是一個代碼塊,當完成了網路請求和更新介面後,應該調用這個代碼塊完成回調動作。

執行completionHandler時,系統會估配量序進程消耗的電量,並根據傳入的UIBackgroundFetchResult參數記錄新資料是否可用。而在調用過程中,應用的後台快照將被更新,對應的app switcher也會被更新。

在實際應用時,我們應當將completionHandler傳遞到應用程式的子組件或儲存起來,然後在處理完資料和更新介面後調用。在這個Demo中,我將completionHandler儲存在全域的程式委託中:

 

#import typedef void (^CompletionHandler)(UIBackgroundFetchResult);@interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window;+ (instancetype)sharedDelegate;@property (copy, nonatomic) CompletionHandler completionHandler;@end

對應的委託方法代碼為:

 

 

- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(CompletionHandler)completionHandler {    NSLog(@Application Delegate: Perform Fetch);        UINavigationController *naviController = (UINavigationController *)self.window.rootViewController;    WebTableViewController *webTableController = (WebTableViewController *)naviController.topViewController;    self.completionHandler = completionHandler;    [webTableController updateBackgroundFetchResult];        application.applicationIconBadgeNumber += 1;}

 

application.applicationIconBadgeNumber +=1;表示當收到一個background fetch請求時,就為使用者在springboard上給一個小提示。(個人是非常討厭這個東西的,也不喜歡用這個東西。)



webTableController是這個Demo中展示內容的關鍵區段,我們在debug時可以類比background fetch模式,在後台抓取到新的資料後,我們就更新webTableController中的表格。

 

- (void)updateBackgroundFetchResult {    WebItem *item = [WebSimulator getNewWebItem];    [self.webContents insertObject:item atIndex:0];        NSMutableArray *updateContents = [NSMutableArray array];    [updateContents addObject:[NSIndexPath indexPathForItem:0 inSection:0]];    [self.tableView insertRowsAtIndexPaths:updateContents withRowAnimation:UITableViewRowAnimationFade];        AppDelegate *appDelegate = [AppDelegate sharedDelegate];    appDelegate.completionHandler = NULL;}


 

這裡我使用一個WebSimulator類類比從網路中擷取資料,每次產生一個隨機數,然後產生對應的URL返回。方法如下:

 

+ (WebItem *)getNewWebItem {    unsigned int randomNumber = arc4random() % 4;        NSMutableDictionary *webInfo = [NSMutableDictionary dictionary];        switch (randomNumber) {        case 0:            webInfo[TITLE_KEY]  = BAIDU;            webInfo[WEBURL_KEY] = BAIDU_URL;            break;                case 1:            webInfo[TITLE_KEY]  = MAIL_126;            webInfo[WEBURL_KEY] = MAIL_126_URL;            break;                    case 2:            webInfo[TITLE_KEY]  = SINA;            webInfo[WEBURL_KEY] = SINA_URL;            break;                    case 3:            webInfo[TITLE_KEY]  = SOGOU;            webInfo[WEBURL_KEY] = SOGOU_URL;            break;                    default:            webInfo[TITLE_KEY]  = BAIDU;            webInfo[WEBURL_KEY] = BAIDU_URL;            break;    }        NSLog(@抓取到的網路內容:%@, webInfo[TITLE_KEY]);    return [[WebItem alloc] initWithWebInfo:webInfo];}


 

 

此時需要在表格中載入新插入的cell:

 

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {    WebCell *cell = (WebCell *)[tableView dequeueReusableCellWithIdentifier:@CellIdentifier forIndexPath:indexPath];        WebItem *item = self.webContents[(NSUInteger)indexPath.row];        [cell configureCellWithWebItem:item];        return cell;}

而configureCellWithWebItem:方法在自訂的WebCell類中:

 

 

- (void)configureCellWithWebItem:(WebItem *)item {    self.showInfo_label.text = item.title;    [self showWebContent:item.webURL];}- (void)showWebContent:(NSURL *)url {    CompletionHandler handler = [AppDelegate sharedDelegate].completionHandler;    NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];    NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];    NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:                                  ^(NSData *data, NSURLResponse *response, NSError *error) {                                      if (error) {                                          if (handler != NULL) {                                              handler(UIBackgroundFetchResultFailed);                                          }                                          return;                                      }                                                                            if (data && data.length > 0) {                                          dispatch_async(dispatch_get_main_queue(), ^{                                              [self.content_webView loadData:data MIMEType:nil textEncodingName:nil baseURL:nil];                                          });                                          if (handler != NULL) {                                              handler(UIBackgroundFetchResultNewData);                                          }                                      }                                      else {                                          if (handler != NULL) {                                              handler(UIBackgroundFetchResultNoData);                                          }                                      }                                  }];    [task resume];}

 

 

以上方法的作用是啟動一個NSURLSession的DataTask,用來載入WebSimulator產生的URL中的資料。

在Data Task的completionHandler代碼塊中,我們根據error和data或response來確定Data Task是否執行成功,然後執行background fetch的completion handler(通過[AppDelegatesharedDelegate].completionHandler擷取)來更新程式快照等。注意在適當的地方要將本次的completion handler清空,否則會影響到表格的reloadData(當我們不需要回調動作時)。

 

來測試一下運行結果:

1.先運行程式,app剛剛啟動時表格只有一行。隨後進入後台,接著進行background fetch類比:

 

2.可以看到springboard上程式有一個提示:

 

3.開啟app switcher可以看到app的快照更新了:

 

4.進入程式可以看到表格變成了兩行(每次background fetch只插入一行新的內容),控制台輸出如下:

 

2014-02-13 03:20:48.541 BackgroundFetch[5406:70b] Application Delegate: Did Finish Lauching2014-02-13 03:20:48.542 BackgroundFetch[5406:70b] Launched in background 02014-02-13 03:20:48.547 BackgroundFetch[5406:70b] 抓取到的網路內容:搜狗2014-02-13 03:20:48.611 BackgroundFetch[5406:70b] Application Delegate: Did Become Active2014-02-13 03:20:53.863 BackgroundFetch[5406:70b] Application Delegate: Will Resign Active2014-02-13 03:20:53.865 BackgroundFetch[5406:70b] Application Delegate: Did Enter Background2014-02-13 03:20:59.130 BackgroundFetch[5406:70b] Application Delegate: Perform Fetch2014-02-13 03:20:59.130 BackgroundFetch[5406:70b] 抓取到的網路內容:百度2014-02-13 03:20:59.342 BackgroundFetch[5406:6a33] 後台抓取結果:UIBackgroundFetchResultNewData2014-02-13 03:27:22.843 BackgroundFetch[5406:70b] Application Delegate: Will Enter Foreground2014-02-13 03:27:22.845 BackgroundFetch[5406:70b] Application Delegate: Did Become Active

由Lauched in background 0可以看到程式是之前就運行了的,並不是從後台啟動的。

 

在app did enter background後,我們進行background fetch,此時在後台中的app將被喚醒,並執行委託中的perform fetch方法,在執行完後台抓取任務後,completion handler最後執行。

 

5.另外可以設定成另外一種啟動模式:程式之前並沒有運行(包括不在後台中),在經過一定的周期後(類似於一個定時器)程式將被系統喚醒並在後台啟動,可以在scheme中更改:

 

雙擊開啟的其中一個scheme(當然也可以另外建立一個scheme,專門設定為後台啟動模式),設定如下:

 

接著啟動程式,控制台輸出:

 

2014-02-13 03:40:21.499 BackgroundFetch[5594:70b] Application Delegate: Did Finish Lauching2014-02-13 03:40:21.500 BackgroundFetch[5594:70b] Launched in background 12014-02-13 03:40:21.505 BackgroundFetch[5594:70b] 抓取到的網路內容:新浪2014-02-13 03:40:21.573 BackgroundFetch[5594:70b] Application Delegate: Perform Fetch2014-02-13 03:40:21.573 BackgroundFetch[5594:70b] 抓取到的網路內容:百度2014-02-13 03:40:21.769 BackgroundFetch[5594:4d03] 後台抓取結果:UIBackgroundFetchResultNewData

可以看到程式是從後台啟動的:Lauched in background 1,而一啟動就進行background fetch操作,springboard的app也收到了提示:

 

 

當然app switcher也被更新了。

 

可以看到,background fetch最大的好處在於它不需要使用者手工參與到擷取資料中,例如我們平時想看微博的時候,需要手動重新整理一下啊,而有了background fetch,app將定時地重新整理微博,確保我們每次開啟app時看到的都是最新的最及時的資訊,無疑這非常適用於社交應用和天氣應用等。而弊端就是流量問題。

 

需要說明一下的是,在運行Demo時,如果background fetch的時間間隔過短會出現如下錯誤:

 

2014-02-13 03:50:57.602 BackgroundFetch[5649:7513] bool _WebTryThreadLock(bool), 0xa173900: Tried to obtain the web lock from a thread other than the main thread or the web thread. This may be a result of calling to UIKit from a secondary thread. Crashing now...1   0x514a1ae WebThreadLock2   0x44c3a7 -[UIWebDocumentView setFrame:]3   0x6d6106 -[UIWebBrowserView setFrame:]4   0x44fd5e -[UIWebDocumentView _resetForNewPage]5   0x450acf -[UIWebDocumentView layoutSubviews]6   0x299267 -[UIView(CALayerDelegate) layoutSublayersOfLayer:]7   0x14d281f -[NSObject performSelector:withObject:]8   0x3b4b2ea -[CALayer layoutSublayers]9   0x3b3f0d4 CA::Layer::layout_if_needed(CA::Transaction*)10  0x3b3ef40 CA::Layer::layout_and_display_if_needed(CA::Transaction*)11  0x3aa6ae6 CA::Context::commit_transaction(CA::Transaction*)12  0x3aa7e71 CA::Transaction::commit()13  0x3b64430 +[CATransaction flush]14  0x26a296 _UIWindowUpdateVisibleContextOrder15  0x26a145 +[UIWindow _prepareWindowsPassingTestForAppResume:]16  0x23f016 -[UIApplication _updateSnapshotAndStateRestorationArchiveForBackgroundEvent:saveState:exitIfCouldNotRestoreState:]17  0x23f390 -[UIApplication _replyToBackgroundFetchRequestWithResult:remoteNotificationToken:sequenceNumber:updateApplicationSnapshot:]18  0x23fbb6 __61-[UIApplication _handleOpportunisticFetchWithSequenceNumber:]_block_invoke19  0x682a04 ___UIAutologgingBackgroundFetchBlock_block_invoke20  0x3d6d __26-[WebCell showWebContent:]_block_invoke21  0x61d2195 __49-[__NSCFLocalSessionTask _task_onqueue_didFinish]_block_invoke22  0x625f286 __37-[__NSCFURLSession addDelegateBlock:]_block_invoke23  0x113c945 -[NSBlockOperation main]24  0x1195829 -[__NSOperationInternal _start:]25  0x1112558 -[NSOperation start]26  0x1197af4 __NSOQSchedule_f27  0x1ae94b0 _dispatch_client_callout28  0x1ad707f _dispatch_queue_drain29  0x1ad6e7a _dispatch_queue_invoke30  0x1ad7e1f _dispatch_root_queue_drain31  0x1ad8137 _dispatch_worker_thread2

貌似是webview還沒有完成載入資料的任務就被強行要求執行新的任務,所以導致無法擷取線程鎖。當然實際應用中,app不能會那麼頻繁地進行後台擷取的(Apple也不允許)。

 

對於這個問題,還沒有解決,有大大知道的麻煩指點下。所以我修改如下:

 

- (void)updateBackgroundFetchResult {    WebItem *item = [WebSimulator getNewWebItem];    [self.webContents insertObject:item atIndex:0];        NSMutableArray *updateContents = [NSMutableArray array];    [updateContents addObject:[NSIndexPath indexPathForItem:0 inSection:0]];    [self.tableView insertRowsAtIndexPaths:updateContents withRowAnimation:UITableViewRowAnimationFade];        AppDelegate *appDelegate = [AppDelegate sharedDelegate];    if (appDelegate.completionHandler != NULL) {        CompletionHandler handler = appDelegate.completionHandler;        handler(UIBackgroundFetchResultNewData);        appDelegate.completionHandler = NULL;    }}

- (void)showWebContent:(NSURL *)url {//    CompletionHandler handler = [AppDelegate sharedDelegate].completionHandler;    NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];    NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];    NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:                                  ^(NSData *data, NSURLResponse *response, NSError *error) {                                      if (error) {//                                          if (handler != NULL) {//                                              handler(UIBackgroundFetchResultFailed);//                                              NSLog(@後台抓取結果:UIBackgroundFetchResultFailed);//                                          }                                          return;                                      }                                                                            if (data && data.length > 0) {                                          dispatch_async(dispatch_get_main_queue(), ^{                                              [self.content_webView loadData:data MIMEType:nil textEncodingName:nil baseURL:nil];                                          });//                                          if (handler != NULL) {//                                              handler(UIBackgroundFetchResultNewData);//                                              NSLog(@後台抓取結果:UIBackgroundFetchResultNewData);//                                          }                                      }                                      else {//                                          if (handler != NULL) {//                                              handler(UIBackgroundFetchResultNoData);//                                              NSLog(@後台抓取結果:UIBackgroundFetchResultNoData);//                                          }                                      }                                  }];    [task resume];}

經測試無錯誤。

 

 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.