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。 和包一起归档。这是唯一一件事后补不回来的事。
- 每个页面打一条面包屑日志。 成本极低,能把一条读不懂的栈变成一个故事。
- 不要 catch 之后一声不吭。 有报告的崩溃,好过悄无声息的数据损坏。
- 把 jetsam 报告当内存 bug 读,不要当崩溃读。那是完全不同的一次排查。
