移动端Crash日志基础:iOS .crash文件结构归纳总结
搞 iOS 稳定性的同学,每天打交道最多的东西就是 .crash 文件。这东西看着像天书,其实结构很固定。我把常见字段拆开揉碎,你记住一张表就够了。
一、.crash 文件长什么样?
随便打开一个 .crash 文件,你会发现它被分成了几个大块。我用个实际案例给你演示:
Incident Identifier: ABCDEF12-3456-7890-ABCD-EF1234567890
CrashReporter Key: a1b2c3d4e5f6
Hardware Model: iPhone12,3
Process: MyApp [1234]
Path: /private/var/containers/Bundle/Application/...
Identifier: com.example.myapp
Version: 1.0.0 (42)
Code Type: ARM-64
Role: Foreground
Parent Process: launchd [1]
Coalition: 5678
Date/Time: 2024-03-15 14:32:18.1234 +0800
Launch Time: 2024-03-15 14:30:00.0000 +0800
OS Version: iPhone OS 17.3.1 (21D61)
Release Type: User
Baseband Version: 5.11.00
Report Version: 104
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000
Exception Codes: 0x0000000000000001, 0x0000000000000000
Triggered by Thread: 0
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libobjc.A.dylib 0x00000001c3a0a000 objc_msgSend + 16
1 MyApp 0x00000001028e3a00 0x1028e0000 + 14848
2 UIKitCore 0x00000001d8b6b000 -[UIViewController viewDidLoad] + 100
...
看到没?字段之间用空行隔开,每个段落的含义非常明确。
二、核心字段速查表
| 字段 | 含义 | 我的个人经验 |
|---|---|---|
Incident Identifier |
唯一标识,Apple 分配 | 每次 crash 都会生成一个独立的 UUID,用来去重 |
Hardware Model |
设备型号(如 iPhone12,3) | 看到这个就能知道是 iPhone 11 Pro 系列 |
OS Version |
系统版本 + 构建号 | 不同 iOS 版本的 crash 行为可能完全不同 |
Exception Type |
最关键字段:异常类型 | 我靠它一眼判断是野指针还是信号问题 |
Exception Subtype |
异常子类型,比如 KERN_INVALID_ADDRESS | 能告诉你访问了哪个非法地址 |
Triggered by Thread |
发生 crash 的线程编号 | 多数情况是主线程,有时后台线程也需要关注 |
Thread X Crashed |
崩溃线程的完整调用栈 | 从下往上读,最上面的就是 crash 点 |
小技巧: 拿到 .crash 文件先看 Exception Type 和 Exception Subtype,能快速缩小排查范围。比如 EXC_BAD_ACCESS 基本是野指针或内存写越界。
三、异常类型全解析
这里我把常见的异常类型列出来,你收藏一下:
- EXC_BAD_ACCESS — 访问了非法内存地址(最常见)
- EXC_CRASH (SIGABRT) — 主动调用 abort() 或 NSAssert 失败
- EXC_BAD_INSTRUCTION — 执行了非法指令(比如整数除以0)
- EXC_ARITHMETIC — 算术异常(极少见 iOS 上)
- EXC_SOFTWARE — 软件触发的异常,通常是 watchdog 超时
- EXC_BREAKPOINT — 断点指令(一般在调试时出现)
Exception Codes 是辅助信息,比如 EXC_BAD_ACCESS 的 codes 里第一项表示访问类型(0 读,1 写),第二项是目标地址。
注意: 有时候 Apple 的 crash 报告会带 Exception Note: SIMULATED (this is NOT a crash)。别被唬住,那只是因为 app 被系统强杀导致日志模拟的,并非真 crash。
四、调用栈:真正的案发现场
调用栈从 0 开始编号,Thread 0 Crashed 下面的每一行对应一个函数调用。格式是:
二进制名称 模块基址 方法名 + 偏移量
举个例子:
1 MyApp 0x00000001028e3a00 0x1028e0000 + 14848
二进制是 MyApp,地址 0x1028e3a00 是运行时具体位置,后面 0x1028e0000 + 14848 表示相对模块基址的偏移。你用 atos 或 dsym 符号化后,就能得到源码行号。
我的习惯: 每次排查都先看栈顶的 3 行,基本就能定位到出问题的函数。有一次我遇到一个诡异的死锁,栈顶显示 objc_retain,我就直接怀疑是 block 循环引用。查了一圈,还真是。
五、其他重要字段
- Process / Version — 用来确认 crash 发生在哪个版本,我经常在线上版本对比中用到。
- Launch Time / Date/Time — 分析 app 启动多久后 crash,能判断是不是初始化阶段的问题。
- Role — 前台还是后台。后台 crash 很多是内存问题或后台任务超时。
- Low Memory — 如果看到
VM Region Info里有 large 区域,说明内存压力大。
六、.crash 文件结构一览
下面这个 SVG 图总结了整个 .crash 的骨架,你可以用它当速查脑图:
七、实战:如何快速读懂一个 .crash
我给你一套「三秒读日志」步骤:
- 看头部 — 确认设备、系统、App 版本。我曾经遇到过只有 iOS 16.4.1 才会崩溃的 case,版本信息非常关键。
- 看异常类型 — 如果是
EXC_CRASH (SIGABRT),大概率是断言或 crashlytics 记录的错误;如果是EXC_BAD_ACCESS,准备调 malloc 调试。 - 看崩溃线程 — 最常见的是 Thread 0。但如果是后台线程崩溃,要考虑多线程安全。
- 看栈顶 — 看前 5 个函数,如果看到
objc_retain、objc_msgSend,十有八九是内存管理问题。我曾经因为一个 block 没 copy,导致所有引用都飞了。
我的经验: 符号化前可以先看看地址偏移范围,如果偏移接近 0,说明 crash 在函数起始处,通常是参数为 nil 或空指针。如果偏移很大,可能是在函数内部深处。
八、常见误区
- 以为
Exception Subtype里的地址就是坏地址 — 不一定,有时只是系统无权限访问的虚拟地址。 - 忽略
VM Region Info— 尤其LARGE类型区域,能帮你判断是不是内存爆了。 - 不看
Triggered by Thread— 例如 SIGABRT 可能由任意线程触发,不能只看 Thread 0。
避坑指南: 我早年看过一个 .crash,主线程栈全是系统方法,一度以为是系统 bug。后来发现 Triggered by Thread 是 3,那是后台线程的 SIGABRT。所以一定不要忽略这个字段。
好了,iOS .crash 文件的结构就这些。记住这张表、这张图、还有那几个排查步骤,你拿到任何 .crash 文件都能快速摸清脉络。后面我们会讲怎么把这些日志符号化、怎么接入线上监控,但前提是你先把底层的结构搞明白。