iOS Crash 的知识梳理
[TOC]
iOS 上的 APP 会遇到各种各样的原因,第三方库不兼容、响应超时、内存等都可能造成 Crash 。但是更多的情况是 程序本身的代码逻辑产生了错误。比如,NSArray等集合容器的越界问题、调用不存在的方法,调用函数参数不符合要求等等。
在开发的过程中,我们可以在 Xcode 上通过一些方法可以捕获到异常,从而定位到出错代码。但是对于已经发布的 APP 来说,想要通过这种方式定位到代码就比较困难了。因此,在 crash 发现之前就去预测到问题代码,这是件非常有意义的事情。
最近,也了解一些这部分的知识,也将看到的知识作为一个梳理,对于后续的项目中 crash 数据的预处理或多或少的提供一种帮助。
1. 常见的 crash 类型
1. 1 OC Exception
由 iOS 库或者各种第三方库或者 OC Runtime 验证出错误而抛出异常。
常见的 OC 异常,包括:
NSInvalidArgumentException // 非法参数异常是 OC 代码中最常出现的错误,比如集合数据的参数传递、一些API的错误使用以及未实现一些方法等。
NSRangeException // 越界异常,常见的场景包括数组最大下标处理错误 | 下标的值是个不确定的变量 | 使用空数组
NSGenericException // 这个异常经常出现在 for in 中,当你 add 或 remove 遍历的数组时,就会出错
NSInternalInconsistencyException // 不一致导致的出错,比如你把 NSDictionary 当作 可变的 NSMutableDictionary 来使用,就会产生这样的错误。还有我们界面使用不当的时候,也会抱这种错误。
NSFileHandleOperationException // 内存空间不足,无法分配足够的内存空间,就会报此类问题。
下图,就是一个 NSRangeException 的异常。
在debug环境下,OC异常导致崩溃时Xcode控制台会输出完整的异常信息。
非debug环境下,可以通过注册 NSUncaughtExceptionHandler 捕获异常信息。
1.2 Mach Exception
Mach 为 XNU 的微内核,Mach 异常是指最底层的内核级异常。在 iOS 系统中,底层 Crash 先触发 Mach 异常,然后再转换为对应的 signal 信号。
- 两种常见的 Mach 异常:
EXC_BAD_ACCESS(Bad Memory Access)
:内存访问错误,分为:访问对象未初始化(SIGBUS 信号) 和访问的东西被回收掉(SIGSEGV 信号)EXC_BAD_INSTRUCTION(Illegal Instruction)
:通过 SIGILL信号触发的。它是在说运行了一条非法的指令。错误格式:`XC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
- 其他类型的异常
1.EXC_RESOURCE
是指的程序到达资源上限,比如cpu占用过高,内存不足之类的
2.EXC_GUARD
是一些c层函数访问错误导致的异常,比如fopen文件访问错误之类的都会爆出这个
3.0x00000020
这种错误基本上都是在 FreeBSD 中。比较常见的就是 看门狗机制。
1.3 Unix Signal Exceptions
从 Mach 异常最终会转化成 Unix 信号投递到出错的线程,如果想要具体了解的可以参考《漫谈iOS Crash收集框架》。
注意:
OC异常并不是真正的异常,但是当一个OC异常被抛出到最外层还没被捕获,程序会强行发送SIGABRT信号中断程序。
Mach异常没有比较便利的捕获方式,既然它最终会转化成信号,我们也可以通过捕获信号,来捕获 Crash 事件。
同时, iOS 提供了 signal 方法来注册一个处理函数,在处理函数中,使用execinfo中的 backtrace_symbols取出汇编层程序的堆栈信息。
void InstallSignalHandler(void) {
signal(SIGHUP, handleSignalException);
signal(SIGINT, handleSignalException);
signal(SIGQUIT, handleSignalException);
signal(SIGABRT, handleSignalException);
signal(SIGILL, handleSignalException);
signal(SIGSEGV, handleSignalException);
signal(SIGFPE, handleSignalException);
signal(SIGBUS, handleSignalException);
signal(SIGPIPE, handleSignalException);
}
void handleSignalException(int signal) {
NSMutableString * crashInfo = [[NSMutableString alloc]init];
[crashInfo appendString:[NSString stringWithFormat:@"signal:%d\n",signal]];
[crashInfo appendString:@"Stack:\n"];
void* callstack[128];
int i, frames = backtrace(callstack, 128);
char** strs = backtrace_symbols(callstack, frames);
for (i = 0; i <frames; ++i) {
[crashinfo appendformat:@"%s\n", strs[i]];
}
[wzcrashreporter savecrash:crashinfo];
}
想要了解各种信号的含义可以学习《iOS异常捕获》
以上,基本上就是我们所能遇到的异常类型了。
2. iOS Crash 日志
2.1 日志结构
一份 iOS crash 日志都包含以下信息:
- 进程信息:闪退进程的相关报告
- Incident Identifier : 崩溃报告的唯一标识符。
- CrashReporter Key:与设备标识相对应的唯一键值
- Hardware Model: 标识设备类型
- Process :对项目的操作权限
- Path:崩溃文件的路径
- 还有一些其他崩溃消息
- 一些基本信息,闪退的时间什么的
- 异常类型:
- Exception Type:异常的类型。
- Exception Codes :异常错误码
- Termination Reason:闪退的原因,比如常见的数组越界啊,什么的。
- Triggered by Thread:出现问题在哪个线程,这个比较重要,首先确定在哪个线程中出了问题,然后再去定位
- 线程回溯:提供应用中所有线程的回溯日志。线程调用的一些堆栈信息。 它包括四列:
- 帧编号
- 调用库以及一些静态二进制库的名称
- 调用方法的地址
- 分为两个子列:基地址和偏移量。这里的方法名是已经被符号化后的状态。
- 线程状态:闪退时寄存器中的值。一般不需要这部分的信息。
- 二进制映像:闪退时已经加载的二进制文件。
2.2 符号化
所谓符号化,就是将 crash 文件线程回溯部分中的基地址+偏移地址转变成 方法名 + 行数。有几种符号化的方法:
-
symbolicatecrash
需要获取到 .dsym文件或 .app 文件 ,和 crash 文件放在同一目录下,执行命令。 -
命令行工具 atos
symbolicatecrash 可以帮助我们很好的分析 crash 日志,但是有它的局限性 — 不够灵活。我们需要 symbolicatecrash、.crash 及 .dSYM 三个文件才能解析。
相对于 symbolicatecrash, atos 命令更加灵活,特别是你需要对不同渠道的 crash 文件,写一个自动化的分析脚本的时候,这个方法优势很大。
但是这种方式也有个不方便的地方:一个线上的 App,用户使用的版本存在差异,而每个版本所对应的 .dSYM 都是不同的。必须确保 .crash 和 .dSYM 文件是匹配的,才能正确符号化,匹配的条件就是它们的 UUID 一致。
有人已经收集完了不同机型的符号文件,可以了解一下。 -
使用集成的工具
具体内容,可以参考iOS Crash 捕获及堆栈符号化思路解析
3.实际应用
通过对上面两部分内容的理解,我们也可以大致的理解和尝试着去定位出错代码。
在项目中,在只能拿到 crash 文件的情况下,去通过上述方式定位到异常位置,找到 crash 的偏移地址,在 Mach-O 文件中进行截取上下文一部分内容,做后续的处理,放入到 CNN 网络中进行学习。
4. 后续
接下来,会去收集更多的 crash 数据,然后试着在 mach-o 文件中进行截取。