iOS 后台数据处理 background fetch

在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的时间周期:

 

?
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模拟:

\

 

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

\

 

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

\

 

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中更改:

\

 

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

\

 

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

 

?
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也收到了提示:

 

\

 

当然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;
     }
}

?
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
- ( 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];
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值