2025-05-24

在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 Lauching
2014-02-13 03:20:48.542 BackgroundFetch[5406:70b] Launched in background 0
2014-02-13 03:20:48.547 BackgroundFetch[5406:70b] 抓取到的網絡內容:搜狗
2014-02-13 03:20:48.611 BackgroundFetch[5406:70b] Application Delegate: Did Become Active
2014-02-13 03:20:53.863 BackgroundFetch[5406:70b] Application Delegate: Will Resign Active
2014-02-13 03:20:53.865 BackgroundFetch[5406:70b] Application Delegate: Did Enter Background
2014-02-13 03:20:59.130 BackgroundFetch[5406:70b] Application Delegate: Perform Fetch
2014-02-13 03:20:59.130 BackgroundFetch[5406:70b] 抓取到的網絡內容:百度
2014-02-13 03:20:59.342 BackgroundFetch[5406:6a33] 後臺抓取結果:UIBackgroundFetchResultNewData
2014-02-13 03:27:22.843 BackgroundFetch[5406:70b] Application Delegate: Will Enter Foreground
2014-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 Lauching
2014-02-13 03:40:21.500 BackgroundFetch[5594:70b] Launched in background 1
2014-02-13 03:40:21.505 BackgroundFetch[5594:70b] 抓取到的網絡內容:新浪
2014-02-13 03:40:21.573 BackgroundFetch[5594:70b] Application Delegate: Perform Fetch
2014-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 WebThreadLock
2   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 _UIWindowUpdateVisibleContextOrder
15  0x26a145 +[UIWindow _prepareWindowsPassingTestForAppResume:]
16  0x23f016 -[UIApplication _updateSnapshotAndStateRestorationArchiveForBackgroundEvent:saveState:exitIfCouldNotRestoreState:]
17  0x23f390 -[UIApplication _replyToBackgroundFetchRequestWithResult:remoteNotificationToken:sequenceNumber:updateApplicationSnapshot:]
18  0x23fbb6 __61-[UIApplication _handleOpportunisticFetchWithSequenceNumber:]_block_invoke
19  0x682a04 ___UIAutologgingBackgroundFetchBlock_block_invoke
20  0x3d6d __26-[WebCell showWebContent:]_block_invoke
21  0x61d2195 __49-[__NSCFLocalSessionTask _task_onqueue_didFinish]_block_invoke
22  0x625f286 __37-[__NSCFURLSession addDelegateBlock:]_block_invoke
23  0x113c945 -[NSBlockOperation main]
24  0x1195829 -[__NSOperationInternal _start:]
25  0x1112558 -[NSOperation start]
26  0x1197af4 __NSOQSchedule_f
27  0x1ae94b0 _dispatch_client_callout
28  0x1ad707f _dispatch_queue_drain
29  0x1ad6e7a _dispatch_queue_invoke
30  0x1ad7e1f _dispatch_root_queue_drain
31  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];
}

經測試無錯誤。

 

 

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *