第3章 ANR日志文件结构解析:traces.txt格式解读、CPU时间片、堆栈调用链、锁信息
3.1 现象识别:traces.txt 是什么?
当应用发生 ANR 时,系统会自动生成一份 /data/anr/traces.txt(低版本)或 /data/anr/anr_* 文件(高版本)。该文件记录了 ANR 发生时系统中所有线程的堆栈快照、CPU 使用统计、锁竞争信息等,是分析 ANR 根因的“第一现场”。
- 文件位置:通常位于
/data/anr/,可通过adb pull /data/anr/traces.txt导出。 - 文件内容:以“线程组”为单位,每个线程包含线程状态、CPU 时间片、调用栈、以及持有的锁/等待的锁。
- 排查入口:优先定位
"main"线程(主线程),其状态和堆栈直接反映 ANR 直接原因。
提示:如果设备是 Android 10+,traces 文件可能分多个文件或位于
/data/anr/ 下的 anr_ 前缀文件中,可用 adb shell ls -l /data/anr/ 确认最新文件。
3.2 数据采集:如何获取并理解 traces.txt 的关键字段
通过以下命令获取完整的 ANR 日志(需要在开发者模式、root 或系统级权限下执行):
adb shell cat /data/anr/traces.txt > traces.txt
adb bugreport bugreport.zip # 推荐全量 bugreport 包含 traces 及其他日志
一份典型的 traces.txt 片段如下(伪代码示意):
----- pid 12345 at 2025-03-21 10:15:30 -----
Cmd line: com.example.myapp
Build fingerprint: 'google/...'
ABI: 'arm64'
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1 obj=0x...
| sysTid=12345 nice=0 cgrp=default sched=0/0 handle=0x...
| state=S schedstat=( 123456 7890 567 ) utm=10 stm=5 core=4 HZ=100
| stack=0x7f...
at com.example.myapp.MainActivity.onClick(MainActivity.java:50)
- waiting to lock <0x0b1c2d3e> (a java.lang.Object) held by thread 2
at android.view.View.performClick(View.java:...
...
"AsyncTask #1" prio=5 tid=2 Runnable
| group="main" sCount=0 dsCount=0 flags=0 obj=0x...
| sysTid=12346 nice=0 cgrp=default sched=0/0 handle=0x...
| state=R schedstat=( 789012 3456 234 ) utm=30 stm=10 core=2 HZ=100
at com.example.myapp.DataLoader.loadData(DataLoader.java:20)
- locked <0x0b1c2d3e> (a java.lang.Object)
at com.example.myapp.DataLoader.run(DataLoader.java:15)
...
我们需要关注以下几个关键字段:
| 字段 | 含义 | 典型值/用途 |
|---|---|---|
tid | 线程 ID | 主线程通常 tid=1 |
| 线程状态 | 如 Runnable、Blocked、Waiting、Sleeping、Native | 主线程若为 Blocked 或 Waiting,大概率是锁等待 |
schedstat | CPU 调度统计(运行时间、等待时间、切换次数) | 单位:纳秒;三个值:运行时间、等待时间、上下文切换次数 |
utm | 用户态 CPU 时间片(单位:jiffies,通常 10ms) | 主线程 utm 偏低说明未获得足够 CPU |
stm | 内核态 CPU 时间片 | 高 stm 可能涉及系统调用、IO 阻塞 |
| 调用栈 | 线程当前执行位置 | 最后一行 at 指示停顿点 |
locked | 该线程持有的锁 | 显示锁对象的十六进制地址 |
waiting to lock | 线程正在等待的锁 | 配合 held by thread X 找到持有者 |
3.3 原因定位:通过 CPU 时间片、堆栈和锁信息推导根因
排查流程如下:
- 定位主线程:找到
"main"线程,查看其状态。若为Runnable且调用栈在框架方法(如View.draw()或Choreographer),说明主线程正在执行大量 UI 操作,CPU 时间片可能被耗尽。 - 检查 CPU 时间片:比较主线程的
schedstat与其他线程。若主线程 utm 远小于其他活跃线程(如 10 vs 300),说明主线程被频繁抢占,或系统整体 CPU 负载高。 - 解析堆栈调用链:主线程停滞在
waiting to lock→ 标记锁地址;查找持有该锁的线程(locked <0x...>);检查持有者线程是否在长时间执行(如 Runnable、IO 调用)。 - 分析锁阻塞:若持有者线程是死循环、长时间同步网络请求或持锁后睡眠,则导致主线程永久阻塞。
注意:主线程状态为
Runnable 并不代表不卡顿——如果其调用堆栈在应用层方法内,而 CPU 时间片极低(schedstat 运行时间不足 100ms),实际可能是系统调度原因导致主线程就绪但未获得 CPU。
判断规则总结:
- 主线程
Blocked+waiting to lock→ 锁竞争问题,转查持有锁线程。 - 主线程
Runnable+ 堆栈在应用层长循环/GC → 应用自身 CPU 饥饿。 - 主线程
Runnable+ 堆栈在 Binder 调用 → IPC 远程异常(如 Provider 无响应)。 - 主线程
Native+schedstat高 stm → 可能涉及内核态阻塞(如文件 I/O、内存映射)。
下面是一张完整的排查路径流程图,帮助理清思路:
3.4 修复动作:针对锁竞争和 CPU 饥饿的代码示例
场景:主线程
Blocked,等待锁 0x0b1c2d3e,该锁被 AsyncTask #1 持有且在 loadData() 中做网络请求。
错误写法(持锁做耗时操作):
// DataLoader.java
private final Object mLock = new Object();
public void loadData() {
synchronized (mLock) { // 持锁后做网络请求
URL url = new URL("https://example.com/api");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// ... 同步读取,耗时2秒
}
}
修复方案:将耗时操作移出同步块,或使用异步回调/协程:
// 修复后:使用 Handler 回调主线程
public void loadData(final DataCallback callback) {
new Thread(() -> {
// 耗时操作不在锁内执行
String result = fetchDataFromNetwork();
synchronized (mLock) {
// 仅保护共享数据更新,不做耗时操作
mData = result;
}
if (callback != null) callback.onDataLoaded(mData);
}).start();
}
针对 CPU 饥饿:检查主线程是否做了大量计算,将其移到后台线程;或使用 Choreographer 分帧处理。
3.5 复测验证:确认修复是否解决 ANR
- 复现原场景,使用
adb shell am start -W观察启动时间;或触发原操作。 - 等待 5 秒以上,确认是否再次出现 ANR 弹窗。
- 再次获取 traces.txt,查看主线程状态是否变为
Runnable且无waiting to lock。 - 使用
systrace或Perfetto抓取 Main Thread 调度情况,验证 utm 提升。
3.6 预防方案:代码规范与监控
| 实践 | 说明 |
|---|---|
| 避免主线程锁竞争 | 所有耗时锁操作移至后台,主线程仅加锁读写简单共享变量 |
使用 StrictMode | 开发期开启磁盘读/写、网络访问在主线程的检测 |
| 线程命名规范 | 创建线程时赋予有意义名称,便于 traces 中快速定位 |
监控 ApplicationNotResponding | 接入第三方 ANR 监控(如 Matrix、BlockCanary)实时收集 |
| 定期分析 traces | 在灰度测试阶段自动收集并解析锁等待链 |
本章核心要点:
- traces.txt 是 ANR 分析的“数据源”,主线程状态和 CPU 时间片是最先入口。
schedstat中的utm反映用户态 CPU 使用,stm反映内核态。- 锁等待链需通过
waiting to lock和locked交叉定位持有者线程。 - 修复方向:缩短持锁时间、将耗时操作移出主线程、优化调度。