如何阅读 iOS 崩溃日志
初次接触时,崩溃日志看起来难以理解。实际上它只是信息密度高,而且大部分内容与您的 bug 无关。下面介绍阅读顺序,以及几处几乎能直接给出答案的位置。
从正确的设备上获取日志
崩溃报告保存在发生崩溃的那台设备上,而不是一台相似的设备,也不是您手边的设备。遇到崩溃的人,日志就在他的手机里。
获取方式有三种:在设备的 设置 → 隐私与安全性 → 分析与改进 → 分析数据 中查找,但那里是一长串命名晦涩的文件,非开发者很难找到;使用崩溃上报 SDK,但它只上报设计范围内的内容;或者使用桌面工具直接从设备中列出。无论采用哪种方式,都要获取文件本身,弹窗截图并不是崩溃日志。
先进行符号化
未符号化的报告只是一串地址:
0 MyApp 0x00000001045a3b2c 0x1044f4000 + 719148符号化之后,同一行会显示函数名和行号。前提是符号表与崩溃的二进制严格对应,即版本号和构建号都一致。请为每个发布的包保留 dSYM;缺少它,栈帧将始终是十六进制地址,事后任何工具都无法还原。
系统栈帧由对应 iOS 版本的系统符号还原。因此,来自团队中无人安装过的 iOS 版本的报告,有时只能符号化一部分。
按以下顺序阅读
1. 异常类型
这是整个文件中信息量最大的一行。
| 类型 | 含义 |
|---|---|
EXC_BAD_ACCESS (SIGSEGV) |
访问了无效内存,如已释放的对象、野指针 |
EXC_CRASH (SIGABRT) |
未捕获的异常、断言失败,或主动调用 abort() |
EXC_BREAKPOINT (SIGTRAP) |
Swift 运行时陷阱,如强制解包 nil、数组越界、as! 转换失败 |
EXC_RESOURCE |
超出资源限制,通常是内存或持续高 CPU |
0x8badf00d |
看门狗终止:启动或响应过慢,被 iOS 终止 |
0xdead10cc |
挂起状态下仍持有文件锁或共享资源 |
最后两个是十六进制谐音(「ate bad food」与「dead lock」),也是列表中最常被误诊的两项,因为它们都不是代码中的崩溃。
2. Termination description
如果存在这一行,它会直接指出断言或异常。请在查看调用栈之前先阅读它。很多看似「不可能发生」的崩溃,答案就完整地写在这一行中。
3. 崩溃线程
报告中会明确标注崩溃线程。其他二十多条线程可以先忽略,它们当时都处于停止状态。
4. 您自己的栈帧
在崩溃线程中向下查找属于您的二进制的栈帧。上方的系统栈帧通常只说明调用路径,如 UIApplicationMain、runloop、代理回调。您自己代码中最靠上的那一帧,才是阅读的起点。
两类并非崩溃的条目
Jetsam 报告。 进程因内存不足被终止。调用栈只显示进程当时所处的位置,并非原因。栈顶没有 bug,bug 在于此前几分钟的内存曲线。请结合内存采集数据交叉验证:内存呈台阶形持续上升、从不回落,最后被终止,就是它的特征。
看门狗终止。 主线程阻塞时间过长,被 iOS 终止。调用栈会显示阻塞位置(通常是主线程上的同步文件读取、锁或网络调用),但不一定能说明阻塞时间为何如此之长。启动阶段出现 0x8badf00d,基本意味着 application(_:didFinishLaunchingWithOptions:) 中执行了过多工作。
日志无法解释时
可按顺序参考以下三条线索:
- 崩溃前的几行日志。 App 最后输出的日志往往比调用栈更有价值,可以说明它当时在做什么。
- 同一时刻的其他活动。 查看该时间点附近的使用记录,确认是否有其他程序在大量占用设备资源。
- 设备本身。 系统版本、剩余存储空间、电池健康度。「只在那台手机上出现」往往意味着「只在磁盘快满时出现」。
有助于排查的习惯
- 保留每一个 dSYM。 与安装包一起归档。这是唯一事后无法补救的一项。
- 每个页面输出一条面包屑日志。 成本极低,却能把一段难以理解的调用栈还原成完整的操作过程。
- 不要捕获异常后不做任何处理。 有报告的崩溃,好过无声无息的数据损坏。
- 把 jetsam 报告当作内存问题处理,而不是当作崩溃处理。两者的排查思路完全不同。
