克魔助手
全部文章

iOS 崩溃日志到底该怎么读

4 分钟阅读崩溃调试

崩溃日志前几次看都像天书。其实不是 —— 它只是信息密度高,而且大部分内容和你的 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:) 里干了太多活。

日志解释不了的时候

三条后续线索,按顺序:

  1. 崩溃前的那几行日志。 App 的临终遗言常常比调用栈更有用。它当时在干什么?
  2. 同一时刻还发生了什么。 那个时间戳附近的使用记录:是不是有别的东西在猛用设备?
  3. 设备本身。 系统版本、剩余存储、电池健康。“只在那台手机上”往往等于“只在磁盘快满的时候”。

让这件事变简单的习惯

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