第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,大概率是锁等待
schedstatCPU 调度统计(运行时间、等待时间、切换次数)单位:纳秒;三个值:运行时间、等待时间、上下文切换次数
utm用户态 CPU 时间片(单位:jiffies,通常 10ms)主线程 utm 偏低说明未获得足够 CPU
stm内核态 CPU 时间片高 stm 可能涉及系统调用、IO 阻塞
调用栈线程当前执行位置最后一行 at 指示停顿点
locked该线程持有的锁显示锁对象的十六进制地址
waiting to lock线程正在等待的锁配合 held by thread X 找到持有者

3.3 原因定位:通过 CPU 时间片、堆栈和锁信息推导根因

排查流程如下:

  1. 定位主线程:找到 "main" 线程,查看其状态。若为 Runnable 且调用栈在框架方法(如 View.draw() 或 Choreographer),说明主线程正在执行大量 UI 操作,CPU 时间片可能被耗尽。
  2. 检查 CPU 时间片:比较主线程的 schedstat 与其他线程。若主线程 utm 远小于其他活跃线程(如 10 vs 300),说明主线程被频繁抢占,或系统整体 CPU 负载高。
  3. 解析堆栈调用链:主线程停滞在 waiting to lock → 标记锁地址;查找持有该锁的线程(locked <0x...>);检查持有者线程是否在长时间执行(如 Runnable、IO 调用)。
  4. 分析锁阻塞:若持有者线程是死循环、长时间同步网络请求或持锁后睡眠,则导致主线程永久阻塞。
注意:主线程状态为 Runnable 并不代表不卡顿——如果其调用堆栈在应用层方法内,而 CPU 时间片极低(schedstat 运行时间不足 100ms),实际可能是系统调度原因导致主线程就绪但未获得 CPU。
判断规则总结:
  • 主线程 Blocked + waiting to lock → 锁竞争问题,转查持有锁线程。
  • 主线程 Runnable + 堆栈在应用层长循环/GC → 应用自身 CPU 饥饿。
  • 主线程 Runnable + 堆栈在 Binder 调用 → IPC 远程异常(如 Provider 无响应)。
  • 主线程 Native + schedstat 高 stm → 可能涉及内核态阻塞(如文件 I/O、内存映射)。

下面是一张完整的排查路径流程图,帮助理清思路:

图3-1 从 traces.txt 定位主线程 ANR 原因的排查路径 获取 traces.txt 定位 "main" 线程 状态? 锁等待分析 CPU 时间片检查 找到锁持有线程 分析持有者堆栈 对比 utm/stm 值 定位阻塞根因并修复 若主线程状态为 Sleeping, 检查堆栈是否在 native sleep

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 交叉定位持有者线程。
  • 修复方向:缩短持锁时间、将耗时操作移出主线程、优化调度。