iOS SIGKILL 信号量崩溃抓取以及优化实践
一. 什么是SIGKILL崩溃
Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Termination Reason: Namespace RUNNINGBOARD, Code 0xdead10cc
一般来说,Apple 崩溃日志里面通常都会包含应用程序被杀死的具体的原因。如上所示,Termination Reason 里面就包含了这个崩溃的错误代码 0xdead10cc,就表示应用程序挂起的时候发生了文件和数据库锁操作而被操作系统杀死。
二. 怎么抓取 SIGKILL 崩溃
丨1 为什么SIGKILL不能被捕获
丨2 使用 MetricKit 框架捕获SIGKILL
丨2.1 Metrickit 是什么
丨2.2 使用Metrickit 收集 SIGKILL信号量 的好处
不需要注册信号量捕获回调函数
不需要时刻监控,只需冷启阶段注册获取一次就行
丨2.3 怎么使用 Metrickit 获取崩溃信息
丨2.3.1 添加 MetricKit 动态库依赖
丨2.3.2 注册 MetricKit 监听者
if (@available(iOS 14.0, *)) {
MXMetricManager *manager = [MXMetricManager sharedManager];
if (self && manager && [manager respondsToSelector:@selector(addSubscriber:)]) {
[manager addSubscriber:self];
}
}
丨2.3.3 监听者实现MXMetricManagerSubscriber协议方法,payloadDic里面包含着上次本应用发生的崩溃日志堆栈和信息
// 用户如果有崩溃数据,注册监听之后就会回调
- (void)didReceiveDiagnosticPayloads:(NSArray<MXDiagnosticPayload *> * _Nonnull)payloads API_AVAILABLE(ios(14.0)){
if (@available(iOS 14.0, *)) {
for (MXDiagnosticPayload *payload in payloads) {
NSDictionary *payloadDic = [payload dictionaryRepresentation];
});
}
}
}
丨2.3.4 当收到回调消息后,需要对关键信息做组装,获取崩溃堆栈和相关关键信息
NSArray *callStackRootFrames = [dicFrame ArrayValueForKey:@"callStackRootFrames"];
if (callStackRootFrames.count <= 0) {
continue;
}
NSDictionary *dicZero = [callStackRootFrames ObjectAtIndex:0];
int rootIndex = 0;
while (dicZero && dicZero.count > 0) {
//获取Image 的 UUID
NSString *binaryUUID = [dicZero stringValueForKey:@"binaryUUID"];
//获取Image 的 名称
NSString *binaryName = [dicZero stringValueForKey:@"binaryName"];
//获取Image 的加载地址
long long baseAdd = [[dicZero NumberValueForKey:@"offsetIntoBinaryTextSegment"] longLongValue];
//获取崩溃函数的地址
long long address = [[dicZero numberValueForKey:@"address"] longLongValue];
//看上一层调用堆栈的
NSArray *subFrames = [dicZero arrayValueForKey:@"subFrames"];
[strStack appendFormat:@"%d %@ 0x%llx 0x%llx + %lld\n", rootIndex, binaryName, baseAdd, address, address - model.baseAddress];
rootIndex++;
if (subFrames && subFrames.count >= 0) {
dicZero = [subFrames ObjectAtIndex:0];
} else {
dicZero = nil;
}
}
丨2.3.5 使用 Metrickit 收集崩溃的不足
只支持 iOS14 以后的崩溃日志收集;PS:MetricKit是iOS13开始有的框架,但是崩溃日志的支持是iOS14开始支持的。 崩溃日志没有返回具体的崩溃时间和启动时间,崩溃场景信息除了堆栈外没有其余信息,附加信息较少,需要另外的手段来收集
如果使用了段迁移编译技术,主程序 Mach-O 的加载地址和 uuid MetricKit无法给出正确的值,需要例外处理。可通过 Mach-O文件的LC-MAIN入口来获取主程序main函数的地址,从而算出加载其起始地址。
iOS14 的崩溃日志是24小时会回调通知一次,时效性低;iOS15 之后,崩溃日志会在下次启动之后就返回,但经验证,可能有例外情况。
丨3 SIGKILL 日志中 Code 的含义解释
0x8badf00d:
0xc00010ff:
发音 (cool off)。表示操作系统因为过热杀死了应用程序, 关于怎么样使你的程序更高效,更低消耗,可以观看:
a. iOS Performance and Power Optimization with Instruments (https://developer.apple.com/videos/)
b. WWDC session (https://developer.apple.com/videos/)
0xbaadca11:
发音(bad call)。表示 应用程序响应 PushKit 的消息并且CallKit调用失败,从而应用程序被系统杀死。
0xbad22222:
表示因为通过 VoIP 调起程序太频繁而被系统杀死。
0xc51bad01:
watchOS 因为后台任务耗费太多 CPU 时间而被杀死。这就需要优化和减少后台任务的 CPU 时间,提高 CPU 使用效率,或者在后台的时候减少大量任务。
0xc51bad02:
watchOS 因为后台任务不能在初始化时间内完成而杀死 应用程序,减少在后台任务的数量可以解决这个问题。
丨4 百度App常见SIGKILL问题
丨4.1 主线程执行耗时操作太久
当应用程序在阻塞主线程一段时间之后就会被看门狗杀死,一般的耗时事件可能有如下几种情形:
弱网下同步的网络请求 处理大量的数据的任务,比如大的JSON文件或者 3D 模型的加载和处理 触发大量的 Core Data 同步保存操作
触发大量的数据库操作等
主线程解码大图片,解压文件等操作
通用解决方案:
将耗时任务的处理放在子线程处理,等处理完成之后回调给主线程, 类似如下操作,在单独的队列处理任务,处理之后回调 block
- (void)getContentArray:(void (^)(NSArray *resultArray))completeBlock {
dispatch_barrier_async(self.readWriteQueue, ^{
if (completeBlock) {
NSArray *resultArray = [NSArray arrayWithArray:self.array];
completeBlock(resultArray);
}
});
}
丨4.2 主线程和子线程死锁,陷入互相等待的循环
举个栗子:
主线程和子线程在单例初始化的时候陷入死锁 [xxxConfig sharedInstance],见如下崩溃堆栈
Thread 0 Crashed:
0 libsystem_kernel.dylib ___ulock_wait (in libsystem_kernel.dylib) 8
1 libdispatch.dylib __dlock_wait (in libdispatch.dylib) 56
2 libdispatch.dylib __dispatch_once_wait (in libdispatch.dylib) 120
3 BaiduBoxApp +[xxxConfig sharedInstance] (in BaiduBoxApp) 20
4 BaiduBoxApp +[xxxConfig updateABConfig] (in BaiduBoxApp) 0
5 BaiduBoxApp -[xxxManager startOnce] (in BaiduBoxApp) 20
子线程堆栈
Thread 33 :
0 libsystem_kernel.dylib ___ulock_wait (in libsystem_kernel.dylib) 8
1 libdispatch.dylib __dlock_wait (in libdispatch.dylib) 56
2 libdispatch.dylib __dispatch_thread_event_wait_slow (in libdispatch.dylib) 56
3 libdispatch.dylib ___DISPATCH_WAIT_FOR_QUEUE__ (in libdispatch.dylib) 364
4 libdispatch.dylib __dispatch_sync_f_slow (in libdispatch.dylib) 144
.........
9 BaiduBoxApp ___48+[xxxConfig sharedInstance]_block_invoke (in BaiduBoxApp) 0
10 libdispatch.dylib __dispatch_client_callout (in libdispatch.dylib) 20
11 libdispatch.dylib __dispatch_once_callout (in libdispatch.dylib) 32
12 BaiduBoxApp +[xxxConfig sharedInstance] (in BaiduBoxApp) 20
相关链接
[1] Addressing Watchdog Terminations
https://github.com/alibaba/dexposed
[2] Understanding the Exception Types in a Crash Report
https://developer.apple.com/documentation/xcode/understanding-the-exception-types-in-a-crash-report#EXC_CRASH-
[3] MetricKit Framework
https://developer.apple.com/documentation/metrickit?language=objc
[4] Examining the Fields in a Crash Report
https://developer.apple.com/documentation/xcode/examining-the-fields-in-a-crash-report