第4章 解读traces.txt主线程堆栈:锁等待、Binder调用、文件IO、Sleep等典型模式
4.1 现象识别——主线程卡顿时,traces.txt在说什么?
当用户反馈“App点按钮后没反应”或“屏幕冻结几秒”时,生产环境通常没有开发工具attach。此时系统生成的 /data/anr/traces.txt 是唯一能告诉我们“主线程当时在干什么”的现场记录。你需要快速识别堆栈中的模式:是等锁?是等Binder回包?还是在读写文件?
| 堆栈模式 | 典型特征 | ANR概率 |
|---|---|---|
| 锁等待 (Lock) | 以 — waiting to lock <0x...> 为标志,常伴随 held by thread xxx | 高 |
| Binder调用 | 堆栈顶部为 BinderProxy.transact() 或 IPCThreadState::waitForResponse(),无超时保护 | 中高 |
| 文件IO | 包含 FileInputStream.read()、open()、mmap() 等系统调用,时间戳显示长时间等待 | 中 |
| Sleep | 直接看到 Thread.sleep() 或 Object.wait()(非锁等待),且经过长时间才恢复 | 中 |
诊断口诀: 先看主线程状态(RUNNABLE / BLOCKED / WAITING),再看堆栈前三行。不要被子线程的锁等待迷惑,只聚焦 main 线程的堆栈。
4.2 数据采集——如何高效捞取 traces.txt
ANR发生时系统自动生成 traces.txt,但设备厂商可能压缩或限制过期时间。规范采集流程:
- adb pull 时机:复现ANR后立即执行
adb pull /data/anr/traces.txt。 - 多份留存:每次ANR会生成
traces_01.txt、traces_02.txt,全部拉取。 - 时间对齐:用
grep "main" traces.txt | head -5确认主线程时间戳与ANR发生时间是否吻合。 - 若无法复现:启用
dalvik.vm.stack-trace-file=/data/anr/traces.txt系统属性,或使用am force-stop触发。
# 典型拉取命令序列
adb shell ls -la /data/anr/
adb pull /data/anr/traces.txt ./traces_crash1.txt
adb pull /data/anr/traces_01.txt ./traces_extra.txt
# 快速检查主线程是否有锁等待
grep -A 20 "main" traces.txt | grep -E "waiting to lock|held by"
4.3 原因定位——四类模式判读方法
4.3.1 锁等待模式
堆栈中出现 — waiting to lock <0xdeadbeef> (a java.lang.Object),并注明持有者线程ID。你需要找到持有锁的线程,分析其为什么长时间不释放。
// 典型锁等待堆栈片段
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1 obj=0x76c4e470 self=0x7b3c9c0800
| sysTid=12345 nice=0 cgrp=default sched=0/0 handle=0x7b4a5d0548
| state=Blocked stack=0x7ffd2b0000 frame=0x7b3c9c0800
at com.example.MyClass.lockMethod(MyClass.java:42)
— waiting to lock <0x0a3b14c0> (a java.util.HashMap) held by thread 23
at com.example.MainActivity.onClick(MainActivity.java:105)
...
定位要点: 查 thread 23 的堆栈。如果 thread 23 在
sleep 或 wait,说明锁持有者进入了长时间等待,应改为短期锁 + 超时机制。
4.3.2 Binder调用模式
主线程调用系统服务(如 ActivityManager、PackageManager)或跨进程服务时,若服务端响应慢,主线程会卡在 BinderProxy.transact() 上。堆栈特征:
// 主线程卡在 Binder 调用
"main" prio=5 tid=1 Native
| group="main" sCount=1 ...
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(BinderProxy.java:521)
at android.app.IActivityManager$Stub$Proxy.getPackageInfo(IActivityManager.java:5373)
at android.app.ApplicationPackageManager.getPackageInfo(ApplicationPackageManager.java:123)
...
通常需要查看 /proc/自进程/stat 或使用 dumpsys binder 确认Binder线程池状态。若服务端卡慢,应在主线程侧添加异步兜底或超时。
4.3.3 文件IO模式
主线程直接做文件读写(如读SharedPreferences、读大文件、写日志)时,若文件被其他线程锁定或IO繁忙,会导致ANR。堆栈示例:
// 文件IO导致的ANR堆栈
"main" prio=5 tid=1 Native
| group="main" sCount=1 ...
at java.io.FileInputStream.readBytes(Native Method)
at java.io.FileInputStream.read(FileInputStream.java:279)
at java.io.DataInputStream.read(DataInputStream.java:153)
at com.example.Utils.loadConfig(Utils.java:50)
...
陷阱提示: FileInputStream 阻塞不会打印“waiting to lock”,容易被误判为“正常读取”。检查
lsof 或 /proc/self/fd 看是否有其他线程对同一文件加锁。
4.3.4 Sleep模式
主线程直接调用 Thread.sleep() 或 Object.wait()(非锁场景),常见于滥用 Thread.sleep() 做延迟、定时轮询。堆栈明确显示睡眠时间:
// 主线程直接 sleep 导致的ANR
"main" prio=5 tid=1 TimedWaiting
| group="main" sCount=1 ...
at java.lang.Thread.sleep(Native Method)
at com.example.SplashActivity.waitForInit(SplashActivity.java:35)
...
修复方向: 使用 Handler.postDelayed() 或 HandlerThread 替代主线程 sleep。
4.4 修复动作——对症下药
根据模式给出最小侵入修复方案:
| 模式 | 修复动作 | 示例代码片段 |
|---|---|---|
| 锁等待 | 缩小锁范围、改用ReentrantLock带超时、或使用ConcurrentHashMap |
|
| Binder慢调用 | 主线程使用AsyncTask或Kotlin协程,或使用Binder的oneway接口 |
|
| 文件IO | 异步读/写、使用mmap、或迁移至Room数据库 |
|
| Sleep | 用Handler.postDelayed、CountDownTimer、或协程delay |
|
4.5 复测与预防
- 复测场景:在低端设备(2GB RAM)+ 弱网络条件下启动App,反复执行导致ANR的按钮操作,每次操作后执行
adb pull traces.txt确认主线程不再出现对应模式。 - 预防措施:
- 在CI/CD中集成StrictMode,开启
detectDiskReads()和detectUnbufferedIO(),主线程出现IO即fail构建。 - 使用自定义ANR Watchdog(如
BlockCanary),在测试阶段抓取主线程卡顿堆栈,提前暴露隐患。
- 在CI/CD中集成StrictMode,开启
预防清单: 每次代码Review时,检查
runOnUiThread / post 内部是否含有锁、Binder同步调用、文件IO、sleep;只要出现,必须用异步替代。