要总结的话,我预测下一代 iPhone 待机时间会减少,实际可用时间减少,但官方标称的使用时间不变。
在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;
}
|
1
2
|
UIKIT_EXTERN
const
NSTimeInterval UIApplicationBackgroundFetchIntervalMinimum NS_AVAILABLE_IOS(7_0);
UIKIT_EXTERN
const
NSTimeInterval UIApplicationBackgroundFetchIntervalNever NS_AVAILABLE_IOS(7_0);
|
如果这两个值都不需要,也可以在这里自行设定一个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;
}
|
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
|
在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
|
当然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
|
对于这个问题,还没有解决,有大大知道的麻烦指点下。所以我修改如下:
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;
}
|