iOS后台任务beginBackgroundTask和endBackgroundTask成对出现
Posted 想名真难
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS后台任务beginBackgroundTask和endBackgroundTask成对出现相关的知识,希望对你有一定的参考价值。
接到线上有人反馈我们的app经常在后台被杀死, 一开始以为是系统机制问题, 后来发现很大程度上是beginBackgroundTask && endBackgroundTask 没有使用正确, 导致后台任务超时, 然后被系统kill.
1.标准写法
@property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundTask;
// APP进入后台的通知
- (void)applicationDidEnterBackgroundWithNote:(NSNotification *)note
[self _beginBackgroundTask];
// APP回到前台的通知
- (void)applicationWillEnterForegroundWithNote:(NSNotification *)note
[self _endBackgroundTaskIfNeeded];
- (void)_beginBackgroundTask
[self _endBackgroundTaskIfNeeded];
_backgroundTask = [UIApplication.sharedApplication beginBackgroundTaskWithExpirationHandler:^
[self _endBackgroundTaskIfNeeded];
];
- (void)_endBackgroundTaskIfNeeded
if ( _backgroundTask != UIBackgroundTaskInvalid )
[UIApplication.sharedApplication endBackgroundTask:_backgroundTask];
_backgroundTask = UIBackgroundTaskInvalid;
可以通过打印UIApplication.sharedApplication.backgroundTimeRemaining来看最大的后台运行时长,在不同的ios系统上不太一样, 大部分是180秒。
注意:测试此功能不能用Xcode直接debug运行,因为在调试器链接到app的进行的情况下,app是不会在后台被挂起的。
2.是否能递归调用此方法来持续获得执行时间
在beginBackgroundTaskWithExpirationHandler里最后再递归调用[self startTask];
经尝试此方法无效,180秒超时后再次申请,会立刻回调超时的block,并且backgroundTimeRemaining时间一直都是0。
并且由于一直不停的在递归创建和终止后台任务,当Expiration真正到来的时候,一个还有一个创建的任务没有关闭。从而导致违背begin和end成对调用的原则,app被系统强制kill。所以此方法不但不能延长执行时间,还会导致app在180秒后台执行时间到达后,被系统kill的情况。
3.beginBackgroundTaskWithExpirationHandler多次被调用的情况
didEnterBackground每次调用都会触发beginBackgroundTaskWithExpirationHandler来创建新的后台任务,并用backgroundUpdateTask保存任务id,但如果第一次的任务还没有endBackgroundTask之前,应用回到前台,然后再次进入后台,就会重新创建一个新的后台任务,并且backgroundUpdateTask之前保存的id会被覆盖,这就违背了beginBackgroundTaskWithExpirationHandler与endBackgroundTask成对调用的原因。
因为前一个后台任务超时的block回调的时候,其实是end了后一个taskId对应的后台任务,并且把taskId赋值为UIBackgroundTaskInvalid。而后一个后台任务超时的block回调的时候,taskId已经变成了null,对其进行end调用已经无效了,所以相当于没有成对调用begin和end,导致的结果就是:后一个后台任务超时的时候,app被系统强制kill。
所以每一次创建的后台任务都要有一个独立的变量来维护其taskId,如果只有一个后台任务,但是有重入的可能,那么应该在willEnterForeground回调中,把前一个后台任务进行endBackgroundTask操作,这样就不存在taskId被覆盖的问题了。或者是每次didEnterBackground的时候,检查taskId == UIBackgroundTaskInvalid,若不满足该条件,说明taskId已经引用了一个正在进行的后台任务,还没有完成,由于这个后台任务重进前台又切换回后台的情况下,backgroundTimeRemaining会被重置为180秒,所以在这种情况下,也可以直接return
if(backgroundUpdateTask != UIBackgroundTaskInvalid)
return;
4、检查其他三方SDK是否存在问题
此时可以保证我们自己的写法是没有问题的了, 但是很多三方SDK也做了保活措施, SDK大多是通过系统通知来监听的, 需要检查下SDK的调用是否成对出现, 那么就需要使用runtime来hook系统的方法, 一共有3个方法需要hook, 2个开启, 一个结束
- - beginBackgroundTaskWithExpirationHandler:
- - beginBackgroundTaskWithName:expirationHandler:
-
-endBackgroundTask:
在hook之后, 可以拿一个字典来记录, 使用任务ID作为Key, 开启任务保存此任务ID到数组中, 结束任务从字典中移除此ID, 在app的各种场景下进前台,退后台, 查看开启和结束是否成对, 找出没有成对的地方, 打印堆栈信息, 找出没有成对的SDK, 升级SDK或者替换掉.
- (UIBackgroundTaskIdentifier)gcs_beginBackgroundTaskWithExpirationHandler:(void(^ __nullable)(void))handler
UIBackgroundTaskIdentifier identifier = [self gcs_beginBackgroundTaskWithExpirationHandler:handler];
NSLog(@"%s, 开启任务ID: %@", __func__, @(identifier));
NSString *key = [self taskKeyWithTaskIdentifier:identifier];
leftBackgroundTaskDic[key] = [self taskInfo];
return identifier;
- (UIBackgroundTaskIdentifier)gcs_beginBackgroundTaskWithName:(nullable NSString *)taskName expirationHandler:(void(^ __nullable)(void))handler
UIBackgroundTaskIdentifier identifier = [self gcs_beginBackgroundTaskWithName:taskName expirationHandler:handler];
NSLog(@"%s, 开启任务名称:%@, ID: %@", __func__,taskName, @(identifier));
NSString *key = [self taskKeyWithTaskIdentifier:identifier];
leftBackgroundTaskDic[key] = [self taskInfo];
return identifier;
- (void)gcs_endBackgroundTask:(UIBackgroundTaskIdentifier)identifier
NSLog(@"%s, 结束任务ID: %@", __func__, @(identifier));
[self gcs_endBackgroundTask:identifier];
NSString *key = [self taskKeyWithTaskIdentifier:identifier];
leftBackgroundTaskDic[key] = nil;
以上是关于iOS后台任务beginBackgroundTask和endBackgroundTask成对出现的主要内容,如果未能解决你的问题,请参考以下文章