移动端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 的骨架,你可以用它当速查脑图:

iOS .crash 文件结构 .crash 文件 头部信息 Incident ID OS Version, Device 异常描述 Exception Type/Subtype Exception Codes 线程调用栈 Thread 0 Crashed 其他线程栈 二进制、版本、进程 触发线程编号 二进制名 + 偏移地址 解读要点 1. 先看 Exception Type,判断根本原因类型 2. 再读 Triggered by Thread 和对应调用栈 3. 结合 OS Version 和 Device 复现场景

七、实战:如何快速读懂一个 .crash

我给你一套「三秒读日志」步骤:

  1. 看头部 — 确认设备、系统、App 版本。我曾经遇到过只有 iOS 16.4.1 才会崩溃的 case,版本信息非常关键。
  2. 看异常类型 — 如果是 EXC_CRASH (SIGABRT),大概率是断言或 crashlytics 记录的错误;如果是 EXC_BAD_ACCESS,准备调 malloc 调试。
  3. 看崩溃线程 — 最常见的是 Thread 0。但如果是后台线程崩溃,要考虑多线程安全。
  4. 看栈顶 — 看前 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 文件都能快速摸清脉络。后面我们会讲怎么把这些日志符号化、怎么接入线上监控,但前提是你先把底层的结构搞明白。