iOS Background Task使用陷阱

发现线上用户有在后台状态被杀死的情况,最终定位到是不合理使用Background Task造成的,这篇文章简要介绍下使用Background Task需要注意的事项。

Background Task相关的API如下:

@interface UIApplication : UIResponder
...
@property(nonatomic,readonly) NSTimeInterval backgroundTimeRemaining

- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:(void(^ __nullable)(void))handler;
- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier;
...
@end

这里直接给出Background Task一般用法:

@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier;
@property (strong, nonatomic) NSTimer *timer;

/*
 进入前台时检查是否有运行中的BackgroundTask,需要及时关闭。
 */
- (void)applicationWillEnterForeground:(UIApplication *)application {
    NSLog(@"easeapi:applicationWillEnterForeground");

    if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
        [application endBackgroundTask:self.backgroundTaskIdentifier];
        self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
    }
}

/*
 进入后台时,检查self.backgroundTaskIdentifier == UIBackgroundTaskInvalid才执行beginBackgroundTaskWithExpirationHandler
 */
- (void)applicationDidEnterBackground:(UIApplication *)application {
    NSLog(@"easeapi:applicationDidEnterBackground");

    //do your background task
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerUpdate:) userInfo:nil repeats:YES];

    if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) return;
    self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^(void) {
        [self.timer invalidate];

        //end background task
        [application endBackgroundTask:self.backgroundTaskIdentifier];
        self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
        NSLog(@"easeapi:beginBackgroundTaskWithExpirationHandler END");
    }];
}

-(void)timerUpdate:(NSTimer *)tim {
    NSLog(@"TIMER :%f", [UIApplication sharedApplication].backgroundTimeRemaining);
}

Background Task几点使用原则:

Background Task仅用于执行短时间的任务

注意和Background Mode的区别。启用Background Mode后(音频播放、后台定位、VoIP等),是没有后台时间限制的。

而Background Task仅用于执行短时间的任务。APP切换到后台后,可以通过beginBackgroundTaskWithExpirationHandler申请一段时间的后台时间,你的任务应该在这段时间内执行完成,否则可能会被系统杀死。

如果不确定任务的执行时间或者是文件读写需要依赖重度IO的操作,不建议使用Background Task。

Background Task最长持续时间到底是多长?

通过以下方式获取后台任务的剩余时间:

NSTimeInterval timeRemaining = [UIApplication sharedApplication].backgroundTimeRemaining

在前台获取时backgroundTimeRemaining为-1(最大值)。切换到后台后backgroundTimeRemaining为后台任务的剩余时间。

Background Task的持续时间并不是一个固定值,在不同性能的设备上差别巨大。在iPhone 6上为20多秒,在高性能的设备上则达到180秒。测试时发现,最长持续时间似乎还和当前的资源占用情况有关。

由于在不同设备上Background Task最长持续时间是不同的,所以开发者应该确保在低性能设备上Background Task的可用时间内可以完成后台任务,否则可能被系统杀死。

beginBackgroundTask和endBackgroundTask必须成对出现

@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier;

如果使用全局的UIBackgroundTaskIdentifier记录后台任务,需要注意每次执行beginBackgroundTask都会生成新的UIBackgroundTaskIdentifier。

self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^(void) {
}];

旧的UIBackgroundTaskIdentifier会被覆盖,则上一个UIBackgroundTaskIdentifier就没有机会执行endBackgroundTask。此时会出现beginBackgroundTask和endBackgroundTask不配对的情况,可能会被系统杀死。

为防止不配对的情况发生,可以仅维护一个全局UIBackgroundTaskIdentifier,在进入前台时检查不为UIBackgroundTaskInvalid,则执行endBackgroundTask。进入后台时,检查不为UIBackgroundTaskInvalid则直接返回。这样,可以保证频繁的切换前后台时,也能保证beginBackgroundTask和endBackgroundTask配对出现。

多次调用beginBackgroundTask并不会获得更多后台时间

在beginBackgroundTask的ExpirationHandler回调中再次执行beginBackgroundTask不会获取更多后台时间。再次执行beginBackgroundTask后,可以获取一个UIBackgroundTaskIdentifier,但此时无法继续执行后台任务了。

其它文章

iOS WKWebView详解及JS Bridge同步调用问题
iOS启动优化之二进制重排
iOS CLLocationManager的弹窗问题
iOS:IDFV(identifierForVendor)使用陷阱