背景
App后台存活时间长短直接对用户体验、业务功能、用户卸载几个方面都有较大的影响。如果存活时间过短,用户每次退到后台后都需要重启App,对于用户来说都是非常不友好的,非常有可能导致用户卸载App,这将是非常大的损失。因此我们着重调研了影响App后台存活时间的具体因素。经过调研发现,除了设备性能影响,后台任务是一个比较重要的方面,大家可能平时并没有重点关注过后台任务对后台存活时间的影响,对后台任务相关的内容并不了解。为了搞清楚后台任务对存活时间的影响和后台任务的使用场景笔者做了深入的调研,一方面是为了解决上面的两个问题,另一方面也是为了对这块的知识做储备。下面笔者将主要介绍一下后台任务使用时存在的坑,以及我们是如何模拟此类场景的。希望这篇文章结能够对你有帮助。
后台任务介绍
什么是后台任务
正常情况下,当App进入后台,会立即变为挂起状态,App中的任何执行都会停止。但是由于需求原因我们并不希望进入后台后马上停止我们的App中的操作,而是让我们的操作执行完成后在变为挂起状态。此时就用到了后台任务,开启后台任务后系统会继续让你的App保持活跃状态,直到任务结束后变为挂起状态。因此通过一句话总结就是,后台任务是系统提供给的让App在后台短暂保持活跃状态的功能。
后台任务与后台常驻App的区别
后台任务只是短暂的让App在后台保持活跃状态,任务结束后App会变为挂起状态,不用额外的权限申请。而后台常驻App是在后台一直保持活跃状态,例如音频类App、地图类App,即使进入后台,App一致是活跃状态。此类型App属于特殊类型,需要申请特殊权限。
后台任务使用
正确开启后台任务
- api介绍
//Marks the start of a task that should continue if the app enters the background.
- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:(void (^)(void))handler;
}
//Marks the end of a specific long-running background task.
- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier;
- 任务开启和结束
UIApplication *app;
UIBackgroundTaskIdentifier wbTaskID;
- (void)WBStartTask{
bgTaskID = [app beginBackgroundTaskWithExpirationHandler:^{
NSLog(@"wb__==: %lu", (unsigned long)wbTaskID);
[app endBackgroundTask:wbTaskID];
}];
}
- (void)WBEndTask{
[app endBackgroundTask:wbTaskID];
}
- 注意事项
beginBackgroundTaskWithExpirationHandler:、endBackgroundTask:必须成对出现,否则会触发存后台任务泄漏。
后台任务的执行时间
不同版本的系统后台任务执行时间也不一样,明细如下:
系统 | 理论时间(s) | 实测时间(s) |
---|---|---|
<= iOS6 | 600 | - |
iOS7 - iOS12 | 180 | 176 |
>= iOS13 | 30 | 26 |
以上时间为打点测试,测试的是任务开始到结束的时间,并不是App由活跃到挂起的时间,挂起时间可以通过xcode控制台进行测试,这里就不详细介绍了。
后台任务对App后台存活时间的影响
错误的使用后台任务都会导致App在后台被系统强杀。主要包括两个方面:后台任务超时和后台任务泄漏。后台任务超时是后台任务规定时间内没有完成相关的耗时操作并且没有结束任务,导致超时;而后台任务泄露是由于开启了后台任务,当任务执行完成后没有将任务结束导致的。因此如果不合理的使用后台任务,App大概率会被系统杀死,大大降低了App在后台的存活时间。
后台任务超时
什么时后台任务超时
一句话总结就是在后台任务中执行耗时操作,任务执行结束后耗时操作仍然在执行。
什么场景会导致后台任务超时
苹果已经给出了任务超时不同场景,笔者对场景进行了模拟,下面是笔者通过demo触发任务超时后的crash日志,通过code码进行了归类。
- 0xbada5e47 开启的后台任务超过阈值,目前阈值为1000个,如果开启的后台任务超过1000,进入后台后会被系统强杀。
复现场景:开启大于1000个后台任务,demo验证后日志如下:
Incident Identifier: 58790B23-E0E9-4650-AC5E-4023A662C7BE
CrashReporter Key: 1610ccc74368b31360a22139adc06b185565627b
Hardware Model: iPhone8,1
Process: OOMTestProjcet [63403]
Path: /private/var/containers/Bundle/Application/11D48709-CDFB-4A60-A730-FA84E8CFB989/OOMTestProjcet.app/OOMTestProjcet
Identifier: wmm.OOMTestProjcet
Version: 1 (1.0)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: wmm.OOMTestProjcet [6715]
Date/Time: 2019-08-29 11:02:07.5557 +0800
Launch Time: 2019-08-29 11:01:47.5072 +0800
OS Version: iPhone OS 11.2.1 (15C153)
Baseband Version: 4.30.02
Report Version: 104
Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Termination Reason: Namespace ASSERTIOND, Code 0xbada5e47
Triggered by Thread: 0
Filtered syslog:
None found
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libsystem_kernel.dylib 0x00000001824c17c4 kevent_id + 8
1 libdispatch.dylib 0x0000000182346498 _dispatch_kq_poll + 208
2 libdispatch.dylib 0x0000000182346e88 _dispatch_event_loop_wait_for_ownership$VARIANT$mp + 432
3 libdispatch.dylib 0x0000000182338b44 _dispatch_sync_wait + 416
4 AssertionServices 0x0000000184eeac54 -[BKSAssertion invalidate] + 84
5 UIKit 0x000000018bedbfc4 -[_UIBackgroundTaskInfo invalidate] + 44
6 UIKit 0x000000018c10d904 -[UIApplication _endBackgroundTask:] + 84
7 libdispatch.dylib 0x000000018232aa14 _dispatch_client_callout + 16
8 libdispatch.dylib 0x0000000182332200 _dispatch_block_invoke_direct$VARIANT$mp + 288
9 FrontBoardServices 0x00000001850a67f8 __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 36
10 FrontBoardServices 0x00000001850a649c -[FBSSerialQueue _performNext] + 404
11 FrontBoardServices 0x00000001850a6a38 -[FBSSerialQueue _performNextFromRunLoopSource] + 56
12 CoreFoundation 0x000000018295697c __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
13 CoreFoundation 0x00000001829568fc __CFRunLoopDoSource0 + 88
此外,还有一种场景会触发0xbada5e47,当在任务到期后执行ExpirationHandler,如果此时ExpirationHandler中存在耗时操作,大概率会触发被系统强杀,错误码同样为0xbada5e47。因此要切记不要在到期ExpirationHandler中执行耗时操作。
- 0xdead10cc这个类型是App挂起之后仍然有访问资源操作导致的,并且网上有人也反馈过此类型,如果挂起后,还存在数据库访问、系统资源访问等,App会马上被系统强杀。因此推测这应该是苹果为保护资源数据强制将App杀死的。
此类型笔者没有模拟出来,有模拟出来的同学可以一起交流一下。
- 0x8badf00d这个code码大家都很熟悉了,这个是出发了苹果的“看门狗”机制,导致App被强杀了,主要是因为主线程卡住的时间太长导致的。后台任务中如果耗时太长卡住时间太久,同样也会触发“看门狗”。
复现场景:1、在后台任务中开启信号量耗时操作,demo验证日志如下:
Incident Identifier: DDF069DB-99FE-4BC7-A21B-AE4D7B0F7EFE
CrashReporter Key: 1610ccc74368b31360a22139adc06b185565627b
Hardware Model: iPhone8,1
Process: OOMTestProjcet [64970]
Path: /private/var/containers/Bundle/Application/07A24A30-F1BE-4816-9167-E71D3E9350B1/OOMTestProjcet.app/OOMTestProjcet
Identifier: wmm.OOMTestProjcet
Version: 1 (1.0)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: wmm.OOMTestProjcet [7485]
Date/Time: 2019-08-29 16:03:07.1169 +0800
Launch Time: 2019-08-29 16:01:50.2218 +0800
OS Version: iPhone OS 11.2.1 (15C153)
Baseband Version: 4.30.02
Report Version: 104
Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Termination Reason: Namespace ASSERTIOND, Code 0x8badf00d
Triggered by Thread: 0
Filtered syslog:
None found
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libsystem_kernel.dylib 0x000000018249f5a4 semaphore_wait_trap + 8
1 libdispatch.dylib 0x000000018232cf04 _dispatch_sema4_wait$VARIANT$mp + 24
2 libdispatch.dylib 0x000000018232d8b4 _dispatch_semaphore_wait_slow + 140
3 OOMTestProjcet 0x00000001029a5440 0x1029a0000 + 21568
4 UIKit 0x000000018bec46b4 -[UIApplication sendAction:to:from:forEvent:] + 96
5 UIKit 0x000000018bec4634 -[UIControl sendAction:to:forEvent:] + 80
6 UIKit 0x000000018beaf1dc -[UIControl _sendActionsForEvents:withEvent:] + 440
7 UIKit 0x000000018bec3f28 -[UIControl touchesEnded:withEvent:] + 576
8 UIKit 0x000000018bec3a48 -[UIWindow _sendTouchesForEvent:] + 2544
9 UIKit 0x000000018bebef60 -[UIWindow sendEvent:] + 3208
10 UIKit 0x000000018be8ff64 -[UIApplication sendEvent:] + 340
11 UIKit 0x000000018c7e531c __dispatchPreprocessedEventFromEventQueue + 2364
12 UIKit 0x000000018c7e78a8 __handleEventQueueInternal + 4760
13 UIKit 0x000000018c7e07c0 __handleHIDEventFetcherDrain + 152
14 CoreFoundation 0x000000018295697c __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
15 CoreFoundation 0x00000001829568fc __CFRunLoop
复现场景:2、开启后台任务,进入后台开始在沙箱中写大量数据,保证写操作超过3分钟,日志如下:
Incident Identifier: 17143F71-7CB2-4A70-A7EA-1862B13C18AE
CrashReporter Key: 1610ccc74368b31360a22139adc06b185565627b
Hardware Model: iPhone8,1
Process: OOMTestProjcet [65618]
Path: /private/var/containers/Bundle/Application/0C35DEAD-4D08-4F40-A0D3-B005580B3FE1/OOMTestProjcet.app/OOMTestProjcet
Identifier: wmm.OOMTestProjcet
Version: 1 (1.0)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: wmm.OOMTestProjcet [7875]
Date/Time: 2019-08-29 17:58:46.0048 +0800
Launch Time: 2019-08-29 17:55:42.5324 +0800
OS Version: iPhone OS 11.2.1 (15C153)
Baseband Version: 4.30.02
Report Version: 104
Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Termination Reason: Namespace ASSERTIOND, Code 0x8badf00d
Triggered by Thread: 0
Filtered syslog:
None found
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libsystem_kernel.dylib 0x00000001824a1014 write + 8
1 Foundation 0x00000001832bf950 _NSWriteToFileDescriptorWithProgress + 216
2 Foundation 0x000000018338ab34 ___NSWriteDataToFileWithExtendedAttributes_block_invoke + 76
3 Foundation 0x00000001832bf864 -[NSData+ 141412 (NSData) enumerateByteRangesUsingBlock:] + 88
4 Foundation 0x00000001832bdc08 _NSWriteDataToFileWithExtendedAttributes + 576
5 Foundation 0x00000001833a7a24 writeStringToURLOrPath + 224
6 OOMTestProjcet 0x0000000102665c5c 0x102660000 + 23644
7 OOMTestProjcet 0x0000000102665960 0x102660000 + 22880
8 UIKit 0x000000018c0f38f0 -[UIApplication workspaceNoteAssertionExpirationImminent:] + 284
9 libdispatch.dylib 0x000000018232aa14 _dispatch_client_callout + 16
10 libdispatch.dylib 0x0000000182332200 _dispatch_block_invoke_direct$VARIANT$mp + 288
11 FrontBoardServices 0x00000001850a67f8 __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 36
12 FrontBoardServices 0x00000001850a649c -[FBSSerialQueue _performNext] + 404
13 FrontBoardServices 0x00000001850a6a38 -[FBSSerialQueue _performNextFromRunLoopSource] + 56
14 CoreFoundation 0x000000018295697c __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
以上两种场景复现的都是ASSERTIOND类型的0x8badf00d
复现场景:3、开启后台任务,在任务块外执行大量的写文件操作,写操作耗时超过3分钟,日志如下:
Incident Identifier: 1AFADC47-B4ED-4E3A-9214-337A21980B76
CrashReporter Key: 1610ccc74368b31360a22139adc06b185565627b
Hardware Model: iPhone8,1
Process: CPUTestTwo [66835]
Path: /private/var/containers/Bundle/Application/BCAC7616-0A9B-419F-BE1C-B4809BC06A01/CPUTestTwo.app/CPUTestTwo
Identifier: wmm.CPUTestTwo
Version: 1 (1.0)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: wmm.CPUTestTwo [8128]
Date/Time: 2019-08-30 11:37:11.3806 +0800
Launch Time: 2019-08-30 11:36:10.0978 +0800
OS Version: iPhone OS 11.2.1 (15C153)
Baseband Version: 4.30.02
Report Version: 104
Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Termination Reason: Namespace SPRINGBOARD, Code 0x8badf00d
Termination Description: SPRINGBOARD, scene-update watchdog transgression: wmm.CPUTestTwo exhausted real (wall clock) time allowance of 10.00 seconds | | ProcessVisibility: Foreground | ProcessState: Running | WatchdogEvent: scene-update | WatchdogVisibility: Foreground | WatchdogCPUStatistics: ( | "Elapsed total CPU time (seconds): 1.870 (user 1.870, system 0.000), 22% CPU", | "Elapsed application CPU time (seconds): 1.284, 15% CPU" | )
Triggered by Thread: 0
Filtered syslog:
None found
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libsystem_kernel.dylib 0x00000001824a1014 write + 8
1 Foundation 0x00000001832bf950 _NSWriteToFileDescriptorWithProgress + 216
2 Foundation 0x000000018338ab34 ___NSWriteDataToFileWithExtendedAttributes_block_invoke + 76
3 Foundation 0x00000001832bf864 -[NSData+ 141412 (NSData) enumerateByteRangesUsingBlock:] + 88
4 Foundation 0x00000001832bdc08
上面场景复现的是SPRINGBOARD类型的0x8badf00d
后台任务泄露
什么是后台任务泄露
后台任务泄露是开启后台任务后当任务执行完成没有执行结束任务操作,导致开启的任务无法结束,此时就出现了任务泄漏。
什么场景会导致后台任务泄露
首先我们来先看一段代码:
- (void)WBStartTask{
self.wbtaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
NSLog(@"wbtaskid__==: %lu", (unsigned long)self.wbtaskId);
[[UIApplication sharedApplication] endBackgroundTask:self.wbtaskId];
}];
}
- (void)WBEndBgTask{
[[UIApplication sharedApplication] endBackgroundTask:self.wbtaskId];
}
上面的代码如果WBStartTask执行两次,就一定会出现后台任务泄露,因为self.wbtaskId第二次会被赋予一个新的id,之前的self.wbtaskId就丢失了,无法正确调用end。日志如下:
Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFYTermination
Reason: Namespace SPRINGBOARD,Code 0x8badf00d
可以看到日志code也是0x8badf00d, 那怎么判断触发0x8badf00d是“看门狗”导致的还是任务泄露导致的呢?这就要看主线程的崩溃日志了,如下:
Thread 0 Crashed:0
libsystem_kernel.dylib 0x000000018472be08 0x18472b000 + 35921
libsystem_kernel.dylib 0x000000018472bc80 0x18472b000 + 32002
CoreFoundation 0x0000000184c6ee40 0x184b81000 + 9744003
CoreFoundation 0x0000000184c6c908 0x184b81000 + 9648724
CoreFoundation 0x0000000184b8cda8 0x184b81000 + 485525
GraphicsServices 0x0000000186b6f020 0x186b64000 + 450886
UIKit 0x000000018eb6d78c 0x18e850000 + 32664447
Messenger 0x0000000103015ee4 0x102ff8000 + 1225968 libdyld.dylib 0x000000018461dfc0 0x18461d000 + 4032
这个日志表示当前的UI线程处于闲置状态,这种状态下被系统杀死,大概率就是因为后台任务泄露导致的。注意:不是说所有的这种日志都是后台任务导致的,而是错误码为0x8badf00d,此时被杀的原因大概率是后台任务泄露,不是绝对的。下面我们来复现一下。
复现场景:1、多次开启后台任务,并且没有在后台任务块中执行endBackgroundTask。2、开启一个后台任务,在ExpirationHandler块中开子线程做耗时操作,然后回到主线程执行endBackgroundTask。3、开启一个后台任务,然后开启子线程执行大量耗时操作,经过demo验证日志如下:
Incident Identifier: 3E16AE66-9CBE-43EC-AABE-CB136E415BAA
CrashReporter Key: 1610ccc74368b31360a22139adc06b185565627b
Hardware Model: iPhone8,1
Process: OOMTestProjcet [65256]
Path: /private/var/containers/Bundle/Application/6D8FA48B-6DCE-4E7B-821E-3A152ED47A04/OOMTestProjcet.app/OOMTestProjcet
Identifier: wmm.OOMTestProjcet
Version: 1 (1.0)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: wmm.OOMTestProjcet [7645]
Date/Time: 2019-08-29 16:57:19.0419 +0800
Launch Time: 2019-08-29 16:54:13.7964 +0800
OS Version: iPhone OS 11.2.1 (15C153)
Baseband Version: 4.30.02
Report Version: 104
Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Termination Reason: Namespace ASSERTIOND, Code 0x8badf00d
Triggered by Thread: 0
Filtered syslog:
None found
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libsystem_kernel.dylib 0x000000018249f568 mach_msg_trap + 8
1 libsystem_kernel.dylib 0x000000018249f3e0 mach_msg + 72
2 CoreFoundation 0x0000000182956308 __CFRunLoopServiceMachPort + 196
3 CoreFoundation 0x0000000182953ed4 __CFRunLoopRun + 1424
4 CoreFoundation 0x0000000182873e58 CFRunLoopRunSpecific + 436
5 GraphicsServices 0x0000000184720f84 GSEventRunModal + 100
6 UIKit 0x000000018bef367c UIApplicationMain + 236
7 OOMTestProjcet 0x0000000100d3d838 0x100d38000 + 22584
8 libdyld.dylib 0x000000018239056c start + 4
此外,在iOS11.0.2上,出现了“看门狗”相关的crash,该crash跟通知相关,这是iOS11系统上的一个bug,在前台收到通知后,进入后台,短时间内会被杀死。该问题苹果官方解释11.2系统上已解决。日志如下:
Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Termination Reason: Namespace <0xF>, Code 0x8badf00d
Triggered by Thread: 0
Filtered syslog:
None found
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libsystem_kernel.dylib 0x0000000186554bc4 mach_msg_trap + 8
1 libsystem_kernel.dylib 0x0000000186554a3c mach_msg + 72
2 CoreFoundation 0x0000000186a03ce4 __CFRunLoopServiceMachPort + 196
3 CoreFoundation 0x0000000186a018b0 __CFRunLoopRun + 1424
4 CoreFoundation 0x00000001869222d8 CFRunLoopRunSpecific + 436
5 GraphicsServices 0x00000001887b3f84 GSEventRunModal + 100
6 UIKit 0x000000018fecf880 UIApplicationMain + 208
7 TerminateTest 0x0000000104d6096c 0x104d58000 + 35180
8 libdyld.dylib 0x000000018644656c start + 4
总结
本文从后台任务与后台常驻App的区别慢慢引出了后台任务的相关技术,并且在后台任务使用上和后台任务存在的壁垒分析了不同场景下使用后台任务存在的问题,以及不同场景下触发的错误日志。笔者希望这些场景分析以及错误日志可以帮助大家更好的了解后台任务,能够在平时使用后台任务时引起警惕,当然这里并不是强调后台任务不好或者后台任务存在风险,只是让大家快速了解并认识后台任务,将来减少踩坑风险,仅此而已。如有纰漏,敬请指正。
参考文献
https://mp.weixin.qq.com/s/r0Q7um7P1p2gIb0aHldyNw 微信内存监控方案
https://www.jianshu.com/p/deee6fedb510 iOS内存分析下-前台内存耗尽闪退(FOOM)
https://satanwoo.github.io/2017/10/18/abort/ iOS内存abort(Jetsam) 原理探究
http://www.cocoachina.com/articles/23281 基于 JetsamEvent 探究 APP 最大内存占用上限
https://engineering.fb.com/ios/reducing-fooms-in-the-facebook-ios-app/ facebook内存警告的处理方案
http://mrpeak.cn/blog/ios-hard-stall-detection/ 检测内存oom的分析
https://juejin.im/post/5c28646f5188257abf1d947d iOS Out-Of-Memory 原理阐述及方案调研
http://www.cocoachina.com/articles/23981 OOM探究:XNU 内存状态管理
https://juejin.im/post/5bec0efcf265da61273cf333 iOS 内存管理研究
https://jinxuebin.cn/2019/07/OOM%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E6%8E%A2%E7%A9%B6/ iOS中OOM底层原理探究
https://blog.csdn.net/u011146511/article/details/76653168 iOS 之苹果运行机制总结
https://www.jianshu.com/p/7c0bec45f7c1 iOS App 后台任务的坑!