第3章 Trace日志的获取与解读

3.1 现象识别:从“无响应”到“有证据”

当用户界面卡顿超过5秒,系统弹出“应用无响应”对话框,现象识别阶段仅能告知“主线程阻塞了”,但阻塞在哪个方法、由哪把锁引起、CPU资源是否充足,这些关键信息都隐藏在系统级Trace中。只有拿到Trace日志,才能从模糊的现象进入精确的数据分析。

常见误区:很多开发者看到ANR后直接修复代码,却不先拉取当时的Trace,导致修复方向跑偏。**Trace是现场回放,必须第一时间采集**。

3.2 数据采集:三种主流获取方式

方式一:adb shell direct(快速抓取最近ANR)

# 获取当前进程的anr trace(需要root或系统权限)
adb shell dumpsys activity processes | grep “anr”

# 或直接拉取系统保存的anr文件
adb shell ls /data/anr/
adb pull /data/anr/traces.txt ./local_traces.txt

# 实时抓取主线程堆栈(非root也可用)
adb shell “ps -A | grep your.package”
adb shell “kill -3 PID”   # 触发主线程dump到logcat

方式二:BugReport(全量系统诊断)

# 生成BugReport(输出为zip包,内含anr/、traces、cpuinfo等)
adb bugreport ./bugreport_$(date +%Y%m%d_%H%M%S).zip

解压后重点关注:

  • FS/data/anr/traces.txt — 每次ANR的完整线程堆栈
  • dumpstate_board.txt — 系统级CPU/IO状态
  • kernel.log — 内核调度延迟证据

方式三:第三方工具实时捕获

用于预防性监控或线上环境回捞:

  • Perfetto:使用config捕获主线程调度延迟
  • 字节码插桩(如Matrix):自动记录长耗时堆栈并上传
  • Profiling API(如Simpleperf):按需采样主线程hot path
采集优先级:发生ANR的瞬间→立即adb pull traces.txt → 5分钟内补拉BugReport → 线上依赖第三方SDK回捞。

3.3 原因定位:Trace关键字段解读

3.3.1 Trace布局结构

一份典型的ANR轨迹文件示例(节选):

----- pid 32712 at 2025-04-07 10:23:45 -----
Cmd line: com.example.app

“main” prio=5 tid=1 Blocked
  | group=”main” sCount=1 dsCount=0 flags=1 obj=0x746c0000
  | sysTid=32712 nice=-10 cgrp=default sched=0/0 handle=0x7291f4a970
  | state=S schedstat=( 12345678 9000000 12 ) utm=8 stm=2 core=0 HZ=100
  | stack=…
  at com.example.ServiceA.methodB(ServiceA.java:45)
  - waiting to lock <0x08c9e540> (a android.util.ArrayMap) held by thread 9
  at com.example.ui.MainActivity.onClick(MainActivity.java:102)
  at android.view.View.pointer……

“Binder:32712_2” tid=9 Runnable
  | state=R schedstat=( 567890 1200000 5 ) utm=4 stm=1
  | stack=…
  at java.util.HashMap.putMap(HashMap.java:900)
  - locked <0x08c9e540> (a android.util.ArrayMap)

3.3.2 关键字段排查表

字段含义判断规则
tid=1主线程(唯一ID为1)tid=1且name=“main”即主线程
Blocked / Runnable线程状态Blocked:等待锁释放;Runnable:实际在执行,需看schedstat
sysTid内核线程ID用于与systrace/sched跟踪关联
schedstatCPU调度统计(runNs/runDelayNs/次数)主线程runNs很小但runDelayNs很大 → 被调度延迟;主线程runNs很大 → 自身耗时
waiting to lock <地址> held by thread X锁等待链找到持有锁的线程X的状态;若X也是Blocked,则形成死锁
state=S / state=RLinux进程状态S=休眠等待(多数是锁/IO);R=运行或可运行

3.3.3 故障树:从Trace到根因

排查逻辑:
  1. 主线程是否为Blocked?
     → 是→找出它等待的锁和持有线程;若持有线程也在Blocked→死锁;若持有线程是Runnable→锁竞争(该线程在临界区耗时过长)
  2. 主线程为Runnable?
     → 检查 schedstat中runNs是否很大(超过200ms)→ 自身方法耗时(IO、计算、等待网络同步)
     → 若runNs很小但runDelayNs很大→ CPU饥饿(其他线程或系统进程占用CPU,主线程得不到时间片)
  3. 主线程为Waiting / Timed_Waiting?
     → 检查等待条件:Object.wait() / Thread.sleep() / Binder调用等 → 通常是异步回调未按时返回或死锁陷阱。

3.4 修复动作 vs Trace验证

根据上述定位结果,采取对应措施(本章仅做示例,后续章节会深度展开):

  • 锁竞争:使用细粒度锁、读写锁、无锁数据结构;将临界区代码移出主线程。
  • 自身耗时:异步化(HandlerThread、协程)、缓存、延迟加载。
  • CPU饥饿:降低后台线程优先级、减少全局锁自旋、优化渲染管线。
  • 死锁:锁排序、超时锁、死锁检测机制。

修复后必须复测:重新生成Trace,对比主线程状态是否回归Runnable且schedstat中的runDelayNs明显下降。

3.5 预防:自动化Trace采集与监控

在CI/CD流程中接入如下手段,防止问题再次上量:

  • 稳定版Android:使用adb shell setprop persist.traces.enable 1永久开启ANR trace写入。
  • 线上APP:集成自定义ANR处理(如FileObserver监听/data/anr/变化),自动上传Trace到后台。
  • 自动化回归:在Monkey测试中实时抓取主线程堆栈,若连续3次看到同一阻塞点则标记为ANR风险。

3.6 Trace解读全景流程图

ANR Trace获取与解读流程图 Trace 获取与解读全景流程 ANR 现象触发 adb shell direct BugReport 第三方工具 提取主线程状态、锁等待、schedstat Blocked? Runnable? Waiting? → 定位根因 修复 → 复测 → 对比Trace改善 自动化采集 & 监控 💡 每一步都需保留原始Trace

3.7 本章要点回顾

  • 三种获取方式:adb direct、BugReport、第三方工具各有适用场景,先拉Trace再修复是铁律。
  • 五个关键字段:tid=1(主线程)、Blocked/Runnable、schedstat、waiting to lock、state=S。
  • 故障树三叉路:Blocked→锁竞争/死锁;Runnable且高runNs→自身耗时;Runnable且高runDelayNs→CPU饥饿。
  • 每次优化后必须重复第3.2~3.4步,用同一设备的Trace对比来证明问题已消除。
避坑指南:不要只看主线程堆栈而忽略其他线程状态。例如“main”被Binder线程持有锁,若Binder线程也在等待另一个锁,很可能形成闭环死锁。务必把持锁线程的stack也拉出来全部审查。