第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跟踪关联 |
schedstat | CPU调度统计(runNs/runDelayNs/次数) | 主线程runNs很小但runDelayNs很大 → 被调度延迟;主线程runNs很大 → 自身耗时 |
waiting to lock <地址> held by thread X | 锁等待链 | 找到持有锁的线程X的状态;若X也是Blocked,则形成死锁 |
state=S / state=R | Linux进程状态 | S=休眠等待(多数是锁/IO);R=运行或可运行 |
3.3.3 故障树:从Trace到根因
排查逻辑:
- 主线程是否为Blocked?
→ 是→找出它等待的锁和持有线程;若持有线程也在Blocked→死锁;若持有线程是Runnable→锁竞争(该线程在临界区耗时过长) - 主线程为Runnable?
→ 检查schedstat中runNs是否很大(超过200ms)→ 自身方法耗时(IO、计算、等待网络同步)
→ 若runNs很小但runDelayNs很大→ CPU饥饿(其他线程或系统进程占用CPU,主线程得不到时间片) - 主线程为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解读全景流程图
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也拉出来全部审查。