3、移动端Crash日志基础:Android Tombstone与ANR日志归纳总结
说到移动端Crash分析,Tombstone和ANR日志是两个绕不开的东西。
我刚开始做Android稳定性时,拿到一份Tombstone文件,满屏的十六进制地址,看得一头雾水。后来踩的坑多了,慢慢就摸清了门道。
说白了,Tombstone就是Native层Crash的“尸检报告”。ANR日志,则是主线程卡死的“审讯记录”。两个文件的侧重点完全不同。
Tombstone日志结构解析
Tombstone文件通常存放在 /data/tombstones/ 目录下。文件名用编号标记,比如 tombstone_00。系统每产生一次Native Crash,就会生成一个新文件,最多保留几个。
我习惯分模块来看。一份完整的Tombstone包含下面几个部分:
- Header – 进程基本信息、Build指纹、Crash时间戳。
- Signal信息 – 触发Crash的信号类型。比如SIGSEGV(段错误)、SIGABRT(主动终止)。
- 寄存器快照 – 各寄存器的值。ARM64架构下从x0到x30、pc、sp、lr等。
- 反汇编代码 – Crash发生处附近的机器码。这一块对定位具体代码很关键。
- 调用栈 – backtrace,包含函数名、偏移地址、so库路径。
- 内存映射信息 – /proc/self/maps的完整快照。
- 进程和线程状态 – 所有线程的堆栈。
- 堆信息 – malloc调试信息,如开启的话会包含分配栈。
// 典型的Tombstone头部示例
Build fingerprint: 'google/taimen/taimen:8.1.0/OPM1.171019.011/4448085:user/release-keys'
Revision: 'rev_1.0'
ABI: 'arm64'
Timestamp: 2024-01-15 14:30:45.123456789+0800
pid: 12345, tid: 12367, name: RenderThread
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
x0 0000000000000000 x1 0000007f8c123456 ...
pc 0000007f8a45b6c8 /system/lib64/libhwui.so
调用栈是重中之重。如果栈里有我们自己的so,大概率能找到函数名。如果没有,那就麻烦了,可能是栈被破坏,或者没带符号表。
我曾经遇到过一个问题:某款手机上了Android 12后,频繁Tombstone,但backtrace里全是??。最后发现是系统so的符号被strip掉了,需要找厂商拿到带符号的版本。
ANR日志结构解析
ANR(Application Not Responding)日志不是一份独立的文件,而是写入了/data/anr/traces.txt(早期版本)或者通过adb shell dumpsys dropbox --print data_app_anr拉取。老版本是单文件,新版本是系统自动上传到DropBox。
ANR日志的核心是告诉我“为什么主线程被卡住”。我把ANR日志的重点拆成这几个部分:
| 部分 | 内容说明 | 排查价值 |
|---|---|---|
| Header | 进程名、pid、ANR类型(InputDispatching/Service/Broadcast) | 确认是哪种ANR,决定线索方向 |
| CPU使用率 | 统计各进程、各线程的CPU占用,包含总的Load值 | 判断是否为CPU负载过高导致 |
| 主线程堆栈 | 主线程的当前调用栈,看卡在哪 | 直接定位卡顿点 |
| 其他线程堆栈 | 所有线程的trace | 分析锁竞争或死锁的关键 |
| 内存信息 | PSS、USS、堆内存使用 | 排查OOM或频繁GC导致的停顿 |
// ANR日志头部示例
ANR in com.example.myapp (com.example.myapp/.MainActivity)
PID: 12345
Reason: Input dispatching timed out (Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 5000.0ms ago.)
Load: 15.86 / 16.01 / 12.34
CPU usage from 12345ms to 0ms ago:
29% 12345/com.example.myapp: 22% user + 7% kernel / faults: 123 minor
12% 678/system_server: 8% user + 4% kernel / faults: 234 minor
5% 456/adbd: 3% user + 2% kernel / faults: 0 minor
...
"main" prio=5 tid=1 Blocked
at com.example.myapp.database.DatabaseHelper.query(DatabaseHelper.java:45)
- waiting to lock <0x1234abcd> (a android.database.sqlite.SQLiteDatabase)
at com.example.myapp.MainActivity.onResume(MainActivity.java:102)
...
看到"Blocked"了吗?主线程在等一把锁,这就是卡死原因。锁被谁持有呢?看下面的线程堆栈,找同样是<0x1234abcd>的线程,通常是后台写入线程。
我在项目里遇到过一个典型场景:ANR总是报"Input dispatching timed out",但主线程堆栈完全看不出问题(栈很干净)。后来回头看CPU信息,发现CPU Load高达15,也就是说系统太忙,主线程虽然没阻塞,但分配不到时间片。这种ANR要降CPU,而不是调代码。
归纳总结:快速定位的思路
学了这么多,怎么用起来?我总结了一个排查流程,用下面这张SVG表示。
总结技巧
- 拿到文件先看时间戳,确认是你关注的Crash。有时候日志会被覆盖。
- Tombstone优先看backtrace,函数名缺失的话,用
addr2line或llvm-symbolizer解析。 - ANR优先看CPU Load。Load高,主线程再干净也白搭。
- 关键字段做成表格,从Tombstone提取signal、fault addr、lib;从ANR提取reason、主线程状态、锁地址。
- 我经常用
grep过滤日志:比如grep -E "signal|backtrace|waiting to lock"。
小经验: 不要一上来就陷入细节。先确定Crash类型(Tombstone还是ANR),再走对应的分支路径。我见过有人拿着ANR日志去查内存映射,完全不搭边。
归纳方法其实就一句话:信号+backtrace=根因,CPU+锁=ANR,两者交叉验证得出最终结论。