3、移动端Crash日志基础:Android Tombstone与ANR日志归纳总结

说到移动端Crash分析,Tombstone和ANR日志是两个绕不开的东西。

我刚开始做Android稳定性时,拿到一份Tombstone文件,满屏的十六进制地址,看得一头雾水。后来踩的坑多了,慢慢就摸清了门道。

说白了,Tombstone就是Native层Crash的“尸检报告”。ANR日志,则是主线程卡死的“审讯记录”。两个文件的侧重点完全不同。

Tombstone日志结构解析

Tombstone文件通常存放在 /data/tombstones/ 目录下。文件名用编号标记,比如 tombstone_00。系统每产生一次Native Crash,就会生成一个新文件,最多保留几个。

关键点: Tombstone记录了Crash时的完整现场,包括寄存器、内存映射、调用栈、堆信息等。

我习惯分模块来看。一份完整的Tombstone包含下面几个部分:

  1. Header – 进程基本信息、Build指纹、Crash时间戳。
  2. Signal信息 – 触发Crash的信号类型。比如SIGSEGV(段错误)、SIGABRT(主动终止)。
  3. 寄存器快照 – 各寄存器的值。ARM64架构下从x0到x30、pc、sp、lr等。
  4. 反汇编代码 – Crash发生处附近的机器码。这一块对定位具体代码很关键。
  5. 调用栈 – backtrace,包含函数名、偏移地址、so库路径。
  6. 内存映射信息 – /proc/self/maps的完整快照。
  7. 进程和线程状态 – 所有线程的堆栈。
  8. 堆信息 – 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
我的习惯: 先看signal信息,确认是SIGSEGV还是SIGABRT。SIGSEGV通常是空指针或野指针,SIGABRT多半是assert失败或主动调用abort()。

调用栈是重中之重。如果栈里有我们自己的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日志头部示例
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 vs ANR 获取原始日志 判断是否为Tombstone或ANR Tombstone → 查Signal + 寄存器 ANR → 查CPU + 主线程堆栈 看backtrace + 内存映射 定位具体so和函数 看锁等待 + 其他线程 确认死锁或资源争抢 关联分析,确认根因 两种日志的关键路径不同:Tombstone偏底层内存,ANR偏上层线程调度。

总结技巧

  • 拿到文件先看时间戳,确认是你关注的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,两者交叉验证得出最终结论。