2. ANR日志分析:读懂系统给你的遗言

ANR 一旦发生,系统会留下两份关键证据——traces.txt 和 logcat。我个人习惯先扫 logcat 快速定位问题范围,再翻 traces.txt 深挖堆栈细节。今天就把这两份日志怎么读、读什么讲透。

2.1 traces.txt 文件结构

文件位置在 /data/anr/traces.txt。你想想看,它是系统在 ANR 触发那一刻,对所有线程做的一次“快照”。结构非常清晰:

  • 头部元数据:进程 ID、时间戳、系统 CPU 负载、进程 CPU 占比等。
  • 线程列表:每个线程一段,以 ----- pid xxxx at yyyy-MM-dd HH:mm:ss ----- 开头。
  • 线程详细信息:包括线程状态、调用栈、持有的锁、等待的锁等。

核心字段速览

----- pid 12345 at 2025-03-20 14:30:00 -----
Cmd line: com.example.app
JNI: CheckJNI is off; workarounds are off; ...
CPU: 0.2% user + 0.1% kernel = 0.3% total
...
"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x75123456
  | self=0x7a123456
  | sysTid=12345 nice=0 cgrp=default sched=0/0 handle=0x7b123456
  | state=S (sleeping) <-- 线程状态
  | heldLock: <0x08abcdef> (a java.lang.Object)  <-- 持有锁
  | waiting on: <0x08deadbeef> (a java.lang.Object)  <-- 等待锁
  | waiting on held by thread: "binder:12345_2" tid=7  <-- 被谁持有
  at java.lang.Object.wait(Native method)
  at com.example.ui.MainActivity.onClick(MainActivity.java:120)
  ...

我曾遇到一个案例:主线程状态是 BLOCKED,等待一个锁,而那个锁被一个子线程持有了 30 秒没释放。顺着 waiting on held by thread 找到对应线程,一看它在做网络请求没有加超时。改完立马解决问题。

2.2 Logcat 中的 ANR 信息

Logcat 是第一条线索。ANR 发生后,系统会打印出几大类日志:

  • ANR in com.example.app – 宣布哪个进程 ANR。
  • am_anr – 系统内部事件日志,包含 ANR 原因标识。
  • CPU usage – 显示各进程 CPU 占用,判断是否被其他进程抢占。
  • Total time / wait time – 超时时间与实际等待时间。

快速抓取命令:

adb logcat | grep -E "ANR|am_anr|traces"

为什么要先看 logcat?因为它告诉你 “为什么判定为 ANR”。比如:

2025-03-20 14:30:00.123  12345-12345  I  am_anr  : [0,12345,com.example.app,1,InputDispatch, ...]

这里面 InputDispatch 说明是输入事件超时,1 表示前台进程,优先级高。这些信息能帮你快速缩小排查方向。

2.3 关键字段速查表

字段 含义 常见值 / 例子
state 线程当前状态 RUNNABLE, BLOCKED, WAITING, NATIVE
heldLock 该线程正持有的锁 <0x08abcdef> (a java.lang.Object)
waiting on 正在等待的锁 <0x08deadbeef>
waiting on held by thread 锁被哪个线程持有着 "binder:12345_2" tid=7
CPU usage 进程 CPU 使用率(logcat 中) 50% user + 10% kernel = 60% total
am_anr 原因 ANR 分类标识 InputDispatch, BroadcastTimeout, ServiceTimeout

2.4 避坑指南:我踩过的几个雷

不要只看主线程堆栈

我曾经接手一个 ANR,主线程明明在 epollWait 休眠,看起来没问题。后来发现是另一个线程死循环占满了 CPU,导致主线程得不到调度。traces.txt 头部上有 CPU usage,一看该进程 CPU 占 95%,立马定位到了那个疯狂循环的线程。所以记得先扫一眼头部负载信息。

锁等待链要追踪到底

看到 waiting on held by thread 后,跳到那个线程的堆栈。如果它也在等另一个锁,继续追。直到找到真正卡住不动的线程。我习惯把这个链画出来——用纸笔或者脑图都行。

2.5 知识体系:ANR 日志分析全流程

下面这张图帮你理清思路:

ANR 日志分析流程 ANR 发生后 traces.txt logcat 头部元数据 (CPU 负载、进程名) 线程堆栈 + 锁信息 查找主线程状态 追踪锁等待链 ANR 类型 (Input/Broadcast/Service) CPU 使用率 / 等待时间 检查系统负载 确认超时原因 综合分析定位根因 (结合锁链 + CPU + 类型)

2.6 实战小技巧:一条命令搞定三份信息

每次 ANR 我都是这么操作的:

# 拉取 traces.txt
adb pull /data/anr/traces.txt

# 同时过滤 logcat 中的 ANR 相关日志
adb logcat -d | grep -E "ANR|am_anr|CPU usage" > anr_log.txt

# 用编辑器(如 VScode)同时打开两个文件,对照着看

这样做的好处是:logcat 告诉你“发生了什么”,traces.txt 告诉你“当时线程在做什么”。两相对照,95% 的 ANR 都能在一刻钟内锁定根因。剩下的 5% 嘛……嗯,可能就要配合 systrace 了,那是后话。