0%

后台任务

三种方式使得iOS程序即使在关闭或崩溃的情况下也能够在后台持续进行一些任务,比如更新程序界面快照,下载文件等。这三个方法分别是Background Fetch,Remote Notification和NSURLSession的backgroundSessionConfiguration

Background Fetch

开启

首先在info plist文件中开启UIBackgroundModes的Background fetch。或者手动编辑这个值

1
2
3
4
5
6
7
8
<key>UIBackgroundModes</key>

<array>

<string>fetch</string>

</array>

iOS默认不进行background fetch,需要设置一个时间的间隔

1
2
3
4
5
6
7
8
9
10
11
12
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{

//UIApplicationBackgroundFetchIntervalMinimum表示尽可能频繁去获取,如果需要指定至少多少时间更新一次就需要给定一个时间值

[application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

return YES;

}

最后在App Delegate里实现下面的方法,这个方法只能在30秒内完成。

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
36
37
38
39
40
41
42
43
44
45
46
- (void) application:(UIApplication *)application

performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler

{

NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];

NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];

NSURL *url = [[NSURL alloc] initWithString:@"http://yourserver.com/data.json"];

NSURLSessionDataTask *task = [session dataTaskWithURL:url

completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

if (error) {

completionHandler(UIBackgroundFetchResultFailed);

return;

}

// 解析响应/数据以决定新内容是否可用

BOOL hasNewData = ...

if (hasNewData) {

completionHandler(UIBackgroundFetchResultNewData);

} else {

completionHandler(UIBackgroundFetchResultNoData);

}

}];

// 开始任务

[task resume];

}

测试

  • 通过查看UIApplication的applicationState
1
2
3
4
5
6
7
8
9
10
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{

NSLog(@"Launched in background %d", UIApplicationStateBackground == application.applicationState);

return YES;

}

Remote Notification

在普通的远程通知里带上content-available标志就可以在通知用户同时在后台进行更新。通知结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13

{

"aps" : {

"content-available" : 1

},

"content-id" : 42

}

接收一条带有content-available的通知会调用下面的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)application:(UIApplication *)application

didReceiveRemoteNotification:(NSDictionary *)userInfo

fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler

{

NSLog(@"Remote Notification userInfo is %@", userInfo);

NSNumber *contentID = userInfo[@"content-id"];

// 根据 content ID 进行操作

completionHandler(UIBackgroundFetchResultNewData);

}

利用NSURLSession进行background transfer task

使用[NSURLSessionConfiguration backgroundSessionConfiguration]创建一个后台任务,当应用退出后,崩溃或进程被关掉都还是会运行。

范例,先处理一条远程通知,并将NSURLSessionDownloadTask添加到后台传输服务队列。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

- (NSURLSession *)backgroundURLSession

{

static NSURLSession *session = nil;

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

NSString *identifier = @"io.objc.backgroundTransferExample";

NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfiguration:identifier];

session = [NSURLSession sessionWithConfiguration:sessionConfig

delegate:self

delegateQueue:[NSOperationQueue mainQueue]];

});

return session;

}

- (void) application:(UIApplication *)application

didReceiveRemoteNotification:(NSDictionary *)userInfo

fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler

{

NSLog(@"Received remote notification with userInfo %@", userInfo);

NSNumber *contentID = userInfo[@"content-id"];

NSString *downloadURLString = [NSString stringWithFormat:@"http://yourserver.com/downloads/%d.mp3", [contentID intValue]];

NSURL* downloadURL = [NSURL URLWithString:downloadURLString];

NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];

NSURLSessionDownloadTask *task = [[self backgroundURLSession] downloadTaskWithRequest:request];

task.taskDescription = [NSString stringWithFormat:@"Podcast Episode %d", [contentID intValue]];

//执行resume保证开始了任务

[task resume];

completionHandler(UIBackgroundFetchResultNewData);

}

下载完成后调用NSURLSessionDownloadDelegate的委托方法,这些委托方法全部是必须实现的。了解所有类型session task的生命周期可以参考官方文档:https://developer.apple.com/library/ios/documentation/cocoa/Conceptual/URLLoadingSystem/NSURLSessionConcepts/NSURLSessionConcepts.html#//apple_ref/doc/uid/10000165i-CH2-SW42

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
36
37
38
39
40
41
42
43
44
#Pragma Mark - NSURLSessionDownloadDelegate

- (void) URLSession:(NSURLSession *)session

downloadTask:(NSURLSessionDownloadTask *)downloadTask

didFinishDownloadingToURL:(NSURL *)location

{

NSLog(@"downloadTask:%@ didFinishDownloadingToURL:%@", downloadTask.taskDescription, location);

// 必须用 NSFileManager 将文件复制到应用的存储中,因为临时文件在方法返回后会被删除

// ...

// 通知 UI 刷新

}

- (void) URLSession:(NSURLSession *)session

downloadTask:(NSURLSessionDownloadTask *)downloadTask

didResumeAtOffset:(int64_t)fileOffset

expectedTotalBytes:(int64_t)expectedTotalBytes

{

}

- (void) URLSession:(NSURLSession *)session

downloadTask:(NSURLSessionDownloadTask *)downloadTask

didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten

totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite

{

}

后台的任务完成后如果应用没有在前台运行,需要实现UIApplication的两个delegate让系统唤醒应用

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

- (void) application:(UIApplication *)application

handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler

{

// 你必须重新建立一个后台 seesiong 的参照

// 否则 NSURLSessionDownloadDelegate 和 NSURLSessionDelegate 方法会因为

// 没有 对 session 的 delegate 设定而不会被调用。参见上面的 backgroundURLSession

NSURLSession *backgroundSession = [self backgroundURLSession];

NSLog(@"Rejoining session with identifier %@ %@", identifier, backgroundSession);

// 保存 completion handler 以在处理 session 事件后更新 UI

[self addCompletionHandler:completionHandler forSession:identifier];

}

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session

{

NSLog(@"Background URL session %@ finished events.\n", session);

if (session.configuration.identifier) {

// 调用在 -application:handleEventsForBackgroundURLSession: 中保存的 handler

[self callCompletionHandlerForSession:session.configuration.identifier];

}

}

- (void)addCompletionHandler:(CompletionHandlerType)handler forSession:(NSString *)identifier

{

if ([self.completionHandlerDictionary objectForKey:identifier]) {

NSLog(@"Error: Got multiple handlers for a single session identifier. This should not happen.\n");

}

[self.completionHandlerDictionary setObject:handler forKey:identifier];

}

- (void)callCompletionHandlerForSession: (NSString *)identifier

{

CompletionHandlerType handler = [self.completionHandlerDictionary objectForKey: identifier];

if (handler) {

[self.completionHandlerDictionary removeObjectForKey: identifier];

NSLog(@"Calling completion handler for session %@", identifier);

handler();

}

}