第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,但设备厂商可能压缩或限制过期时间。规范采集流程:

  1. adb pull 时机:复现ANR后立即执行 adb pull /data/anr/traces.txt。
  2. 多份留存:每次ANR会生成 traces_01.txt、traces_02.txt,全部拉取。
  3. 时间对齐:用 grep "main" traces.txt | head -5 确认主线程时间戳与ANR发生时间是否吻合。
  4. 若无法复现:启用 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
// 原:synchronized(map){...}
ConcurrentHashMap map = new ConcurrentHashMap();
map.put(key, value); // 无需全局锁
Binder慢调用 主线程使用AsyncTask或Kotlin协程,或使用Binder的oneway接口
// 原:直接调用 binderService.getData()
lifecycleScope.launch(Dispatchers.IO) {
    val data = binderService.getData()
    withContext(Dispatchers.Main) { updateUI(data) }
}
文件IO 异步读/写、使用mmap、或迁移至Room数据库
// 原:FileInputStream(...).read(); 在 main 线程
GlobalScope.launch(Dispatchers.IO) {
    val bytes = FileInputStream(path).use { it.readBytes() }
    withContext(Dispatchers.Main) { processBytes(bytes) }
}
Sleep 用Handler.postDelayed、CountDownTimer、或协程delay
// 原:Thread.sleep(5000); 更新UI
Handler(Looper.getMainLooper()).postDelayed({
    updateUI()
}, 5000L)

4.5 复测与预防

  • 复测场景:在低端设备(2GB RAM)+ 弱网络条件下启动App,反复执行导致ANR的按钮操作,每次操作后执行 adb pull traces.txt 确认主线程不再出现对应模式。
  • 预防措施:
    1. 在CI/CD中集成StrictMode,开启 detectDiskReads() 和 detectUnbufferedIO(),主线程出现IO即fail构建。
    2. 使用自定义ANR Watchdog(如BlockCanary),在测试阶段抓取主线程卡顿堆栈,提前暴露隐患。
预防清单: 每次代码Review时,检查 runOnUiThread / post 内部是否含有锁、Binder同步调用、文件IO、sleep;只要出现,必须用异步替代。
traces.txt 主线程堆栈分析决策树 获取 traces.txt 主线程状态是 RUNNABLE / BLOCKED / WAITING? (查看 tid=1 行 state 字段) BLOCKED 锁等待模式 Native 堆栈含 BinderProxy? transact() / waitForResponse() 是 Binder 调用模式 否 文件 IO 模式 WAITING/TimedWaiting 堆栈含 sleep/wait? Thread.sleep / Object.wait 是 Sleep 模式 对应修复动作 • 锁等待 → 缩小锁范围 / 用 ConcurrentHashMap 或 ReentrantLock.tryLock • Binder 调用 → 异步发起或使用 oneway 接口 • 文件 IO → 使用 IO 线程 / mmap / Room • Sleep → 用 Handler.postDelayed 或协程 delay