iOS 7学习:多任务处理之Background Fetch

简单来说,这玩意是对开发者友好,但对设备不友好的(可能会偷偷摸摸地占用流量和电量)。
对用户来说,如果你带宽够,对发热不敏感的话,会得到更好的应用体验。

从 iOS 4 开始,应用就可以在退到后台后,继续运行一小段时间了(10 分钟)。
此外还可以把自己声明为需要在后台运行,就能不限时地运行了。不过限制为播放音乐、使用 GPS 等。值得一提的是,有的应用为了达到后台不限时运行的目的,在后台播放无声的音乐(审核不一定会被发现)。
iOS 5 开始又多了一种类型:下载报刊杂志。
然后 iOS 7 则可以下载各种玩意和定时抓取。

不过对于开发者来说,iOS 7 还有 2 个需要注意的区别:
  1. iOS 7 以前,应用进入后台继续运行时,如果用户锁屏了,那么 iOS 会等待应用运行完,才进入睡眠状态。
    而在 iOS 7 上,系统会很快进入睡眠状态,那些后台应用也就暂停了。如果收到事件被唤醒(例如定时事件、推送、位置更新等),后台应用才能继续运行一会。
    因为处理过程变成了断断续续的,因此下载时也要使用 NSURLSession 来处理(即下文中的 Background Transfer Service)。
  2. 由于 App Switcher 的存在,应用即使在后台,用户仍然能看到它在退出前的快照。如果有什么不可见人的东西(例如刚拍的艳照啦,劲爆的聊天内容啦,银行卡密码什么的),就处理下界面来隐藏吧。
顺便再介绍下三个新东西:
  1. Background Fetch。可以设置应用在后台至少隔多久时间就去抓取一下更新(注意是至少,不保证达到这个频率)。以微博来举例,如果用户的微博被别人评论了,iOS 7 之前得让微博的服务器来推送一条通知,用户接收到后,进入该条微博,等待加载该评论;而 iOS 7 上则可以让用户每隔一定时间(如一分钟)查询一下有没有更新,发现有则下载更新的内容,然后发送一条本地通知给用户,用户接收到后,进入该条微博,显示在后台下载好的评论内容。如果需要的话,连 timeline 都能给你提前更新了。
    很显然,这种做法体验更好,但更费流量和电池。
  2. Silent Remote Notification。iOS 7 之前的推送会直接弹出一个对话框,用户确认后才会进入应用;而 iOS 7 上则可以不弹出对话框,而是直接通知应用去下载更新,等下完后再发送本地通知给用户,这样用户进入应用后,就能直接看到更新的内容了。
    这种做法就比前者好些,不需要在后台轮询了。只是用户如果对更新内容不感兴趣,也会被强制下载。
    为了避免被滥用,这种推送有频率限制(每小时几次),所以推送 QQ、微信这类经常被刷屏的消息肯定没戏。
  3. Background Transfer Service。iOS 7 之前的应用可以在后台继续运行一段时间来下载,但如果因为各种原因而导致应用被退出了(被用户杀掉,内存不足或超时被系统杀掉等),那么下载是得不到保证的。iOS 7 的后台传输服务则可以让系统去下载,出错或下完后通知并唤醒应用来处理。
    对用户来说的好处就是,想离线缓存影片时,可以让优酷之类的应用在后台下载,自己放点音乐看看漫画,而不用傻傻地开着优酷等待下完。上传当然也是支持的,基友再也不用担心你没空发小电影什么的了。此外,1 和 2 里提到的后台下载,也都会用到 3。
    为了避免浪费流量,该服务只会在 WiFi 环境下才进行传输。
需要特别注意的是: 应用被以上三类唤醒时,只被给予几秒钟时间来处理更新。

要总结的话,我预测下一代 iPhone 待机时间会减少,实际可用时间减少,但官方标称的使用时间不变。



在iOS7中,Apple官方为开发者提供了两个可以在后台更新应用程序界面和内容的API。第一个API是后台获取(Background Fetch),该API允许开发者在一个周期间隔后进行特定的动作,如获取网络内容、更新程序界面等等。第二个API是远程通知 (Remote Notification),它是一个新特性,它在当新事件发生时利用推送通知(Push Notifications)去告知程序。这两个新特性都是在后台进行的,这样更加有利于多任务执行。

本文只讲后台抓取内容(Background Fetch)。(在发送远程推送的时候貌似需要证书方面,比较复杂,所以这里没有尝试第二项内容)

多任务的一个显著表现就是后台的app switcher界面(这个在iOS 6越狱插件中就玩过了),该界面会显示出所有后台程序在退出前台时的一个界面快照。当完成后台工作时,开发者可以更新程序快照,显示新内容的预览。例如打开后台的微博我们可以看到badgeNumber提示、qq的信息提示、最新天气情况提示等等。这样使得用户在不打开应用程序的情况下预览最新的内容。后台抓取内容(Background Fetch)非常适用于完成上面的任务。

 

下面来看个Demo。

第一步,为程序配置后台模式:

iOS 7学习:多任务处理之Background Fetch_第1张图片

 

第二步,设置程序的Background Fetch的时间周期:

 

?
1
2
3
4
5
6
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
     [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
     
     return YES;
}

这里的BackgroundFetchInterval可以设置两个值:

 

 

?
1
2
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值。

 

接着是实现非常关键的委托方法:

 

?
1
2
/// 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保存在全局的程序委托中:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
# import <uikit uikit.h= "" >
 
typedef void (^CompletionHandler)(UIBackgroundFetchResult);
 
@interface AppDelegate : UIResponder <uiapplicationdelegate>
 
@property (strong, nonatomic) UIWindow *window;
 
+ (instancetype)sharedDelegate;
 
@property (copy, nonatomic) CompletionHandler completionHandler;
 
@end </uiapplicationdelegate></uikit>

对应的委托方法代码为:

 

 

?
1
2
3
4
5
6
7
8
9
10
- ( 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中的表格。

 

?
1
2
3
4
5
6
7
8
9
10
11
- ( 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返回。方法如下:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
+ (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:

 

?
1
2
3
4
5
6
7
8
9
- (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类中:

 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- ( 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模拟:

iOS 7学习:多任务处理之Background Fetch_第2张图片

 

2.可以看到springboard上程序有一个提示:

iOS 7学习:多任务处理之Background Fetch_第3张图片

 

3.打开app switcher可以看到app的快照更新了:

iOS 7学习:多任务处理之Background Fetch_第4张图片

 

4.进入程序可以看到表格变成了两行(每次background fetch只插入一行新的内容),控制台输出如下:

 

?
1
2
3
4
5
6
7
8
9
10
11
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中更改:

iOS 7学习:多任务处理之Background Fetch_第5张图片

 

双击打开的其中一个scheme(当然也可以另外新建一个scheme,专门设置为后台启动模式),设置如下:

iOS 7学习:多任务处理之Background Fetch_第6张图片

 

接着启动程序,控制台输出:

 

?
1
2
3
4
5
6
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也收到了提示:

 

iOS 7学习:多任务处理之Background Fetch_第7张图片

 

当然app switcher也被更新了。

 

可以看到,background fetch最大的好处在于它不需要用户手工参与到获取数据中,例如我们平时想看微博的时候,需要手动刷新一下啊,而有了background fetch,app将定时地刷新微博,确保我们每次打开app时看到的都是最新的最及时的信息,无疑这非常适用于社交应用和天气应用等。而弊端就是流量问题。

 

需要说明一下的是,在运行Demo时,如果background fetch的时间间隔过短会出现如下错误:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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也不允许)。

 

对于这个问题,还没有解决,有大大知道的麻烦指点下。所以我修改如下:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- ( 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;
     }

你可能感兴趣的:(iOS 7学习:多任务处理之Background Fetch)