Background Task
因为iOS程序切换到后台之后,很有可能被系统杀掉,因此切换到后台的时候需要保存重要数据。UIApplication的这个方法能让系统给App一段时间,执行重要任务。
beginBackgroundTaskWithName:expirationHandler:
系统给了多少时间呢?可以用backgroundTimeRemaining API查看。如果超时,expirationHandler参数的block将会被调用(不超时就不会调用)。
-(void)applicationDidEnterBackground:(UIApplication *)application{
__block UIBackgroundTaskIdentifier taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"fakeTask" expirationHandler:^{
NSLog(@"end");
}];
dispatch_async(dispatch_get_main_queue(), ^{
for (int i = 0; i < 10000; i++){
if(i % 10 == 0){
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
NSTimeInterval timeLeft = [[UIApplication sharedApplication] backgroundTimeRemaining];
NSLog(@"App Status: %ld, task time left: %.2f", (long)state, timeLeft);
}
NSLog(@"counting: %d", i);
}
[[UIApplication sharedApplication] endBackgroundTask:taskId];
taskId = UIBackgroundTaskInvalid;
});
}
需要使用endBackgroundTask来显示的结束task,否则任务就一定会超时,当然超时的时候会执行expirationHandler,但是最后程序会被杀死。
如果不在Background的状态下调用beginBackgroundTaskWithName函数呢?会正常执行。
Background Download
使用URLSession可以让应用程序进行后台下载,即使App被系统杀死了,后台下载仍能顺利完成。
首先需要将URLSession的Config配置成Background
-(NSURLSession *)session{
if(_session == nil){
NSURLSessionConfiguration *conf = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"bk_download"];
conf.sessionSendsLaunchEvents = YES;
conf.discretionary = YES;
conf.networkServiceType = NSURLNetworkServiceTypeBackground;
_session = [NSURLSession sessionWithConfiguration:conf delegate:self delegateQueue:nil];
}
return _session;
}
Background 类型的Session不支持Block形式的Task,要使用delegate
-(void)downloadFileInBackground{
NSString *urlStr = @"http://dg.101.hk/1.rar";
NSURL *url = [NSURL URLWithString:urlStr];
NSURLSessionTask *backgroundTask = [self.session downloadTaskWithURL:url];
backgroundTask.earliestBeginDate = [[NSDate date] dateByAddingTimeInterval:10];
[backgroundTask resume];
}
然后在delegate中写下载完成的回调,这一步和前台的下载一样:
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
//...
}
后台下载完成的回调
-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler{
NSLog( @"handleEventsForBackgroundURLSession");
self.downloadHandler = completionHandler;
self.session; //如果app被系统杀死,要重新创建session。
}
handleEventsForBackgroundURLSession这个回调函数无论App在后台还是被系统杀死,都会调用,如果App被系统杀死了,则要重建URLSession(必须用同一个id)。在这个函数中还要将completionHandler保存起来,在所有任务都完成后,调用这个handler告诉系统,已经处理完成。
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{
NSLog(@"URLSessionDidFinishEventsForBackgroundURLSession");
if(self.downloadHandler){
self.downloadHandler();
}
}
当所有任务执行完毕后,会回到URLSessionDidFinishEventsForBackgroundURLSession这个函数,在这里告诉系统执行完毕吗,可以被Suspend了。
如果程序是在前台,则下载完成不会调用handleEventsForBackgroundURLSession和URLSessionDidFinishEventsForBackgroundURLSession。
同样,用户主动杀死的程序不能使用后台下载功能。
测试时,如何让系统杀死应用程序?
https://forums.developer.apple.com/thread/92687#280376
Background Fetch
Background Fetch可以让App没有启动或者在后台的时候,周期性的获取数据。一些内容类型的App,比如新闻,小说等,可以通过使用Background Fetch技术让用户更快的获取信息。一个最合适的场景描述是:一个新闻App,通过Background Fetch技术在夜里获取了用户关注的最新内容并保存到本地,早上用户在地铁等信号不好的地方打开App,可以直接查看本地保存的最新新闻。
使用Background Fetch功能需要申请Capability:
第二步,在App启动的时候设置请求周期
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
第三步,实现AppDelegate的application:performFetchWithCompletionHandler:方法
-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler{
// 请求数据
completionHandler(UIBackgroundFetchResultNewData);
}
这个方法的执行时间不要超过30秒,实际上越短越好,如果这个方法执行时间过长,iOS系统就会降低Background Fetch的调用频率。
When this method is called, your app has up to 30 seconds of wall-clock time to perform the download operation and call the specified completion handler block.
调试设置
将launch due to background fetch event勾上,然后点击Run按钮启动应用。
注意,Background Fetch不会工作,如果用户主动杀死了App。
https://stackoverflow.com/questions/35478726/background-fetch-is-not-working-after-killing-the-app
Remote Notification
在iOS7以后推送消息的时候可以唤醒App,执行一段代码,也叫静默推送。静默推送和普通推送的流程有些差别:
普通推送,系统收到推送消息之后,应用程序并不做任何事情,等待用户操作。静默推送,在系统收到推送之后,唤起App,App的didReceiveRemoteNotification方法被调用,在这个方法中可以发起一个网络请求,下载数据。
静默推送只需要在普通推送的payload中增加一个content-available的key:
{
"aps":
{
"content-available":1,
"alert":"This is some fancy message2.",
"badge":6,
"sound": "default"
}
}
需要注意的是:
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
NSLog(@"iOS7及以上系统,收到通知:%@", userInfo);
completionHandler(UIBackgroundFetchResultNewData);
}
静默推送唤起的任务最多执行30s的时间,completionHandler必须被调用,告诉系统任务执行完毕。
As soon as you finish processing the notification, you must call the block in the handlerparameter or your app will be terminated. Your app has up to 30 seconds of wall-clock time to process the notification and call the specified completion handler block.
另外,在用户点击了推动消息启动了应用之后,didReceiveNotificationResponse这个方法会被调用,因此要注意数据的处理是否会和didReceiveRemoteNotification重复。
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler{
completionHandler(); // 系统要求执行这个方法
}
同样,如果用户主动杀死了App,静默推动也不会起作用。
其他
如何禁止应用程序在后台运行?
在Info.pist中添加Key UIApplicationExitsOnSuspend,这样应用程序就不会进入Background状态,切换到后台会调用applicationWillTerminate:然后结束。
应用程序后台任务完成后如何通知用户?
可以使用LocalNotification技术。对于静默推送,本身已经通知了用户,不需要再次使用LocalNotification技术。对于IM类型的应用程序,使用静默推送和LocalNotification组合可以优化设计。
其他后台运行的情况
当应用程序需要如下功能的时候,往往会在后台运行:Location, voip, audio, bluetooth, newsstand-content,对应的都需要在Capabilities中注册服务。
官方文档:Background Execution