4. ANR trace文件解读:主线程堆栈、binder调用、锁等待、CPU负载

4.1 现象识别

导航操作触发 ANR 后,系统会生成 /data/anr/traces.txt(Android 10 以下)或 /data/anr/anr_* 压缩包(Android 10+)。从 trace 文件中可以直观看到 主线程 当前执行栈,通常表现为以下三种阻塞模式之一:

  • Binder 调用阻塞:主线程正在等待一次 IPC 响应,栈顶可见 BinderProxy.transact() 或 Binder.execTransact() 停顿。
  • 锁等待(Lock contention):主线程尝试获取某个同步锁(monitor、ReadWriteLock、ReentrantLock)但被其他线程持有,栈顶可见 Object.wait() 或 LockSupport.park() 及相关 Lock 实现类。
  • CPU 竞争/调度延迟:主线程处于 Runnable 状态但长时间未获得 CPU 时间片,trace 中显示大量“running”时间,同时系统 CPU 负载较高。

在导航 ANR 场景中,Binder 调用阻塞 和 锁等待 是最常见的原因,CPU 负载高通常作为并发因素加剧问题。

4.2 数据采集

拿到 ANR trace 文件后,应提取以下关键数据段:

数据项提取位置示例
主线程堆栈"main" prio=5 tid=1 段落
"main" prio=5 tid=1 Blocked
  at com.example.NavigationService.fetchRoute(Native)
  at com.example.NavigationFragment.updateRoute(NavigationFragment.java:85)
  - waiting to lock <0x07c9...> (a java.lang.Object) held by thread 12
Binder 调用栈主线程或 Binder 线程栈中的 BinderProxy.transact()
at android.os.BinderProxy.transact(Native Method)
  at com.example.IRouteService$Stub$Proxy.getRoute(IRouteService.java:56)
  - blocking on binder transaction
锁等待详情主线程后的 Blocked 或 Waiting 状态,以及 held by 提示
- waiting to lock <0x07c9...> (a java.lang.Object) held by thread 12
CPU 负载文件末尾 CPU usage 段
CPU usage from 0ms to 5001ms later:
  57% 2022/com.example.nav: 14% user + 43% kernel / faults: 1234 minor
  23% 1234/kworker/u:12
  ...
采集技巧:使用 adb bugreport 一次性获取 trace + logcat + CPU 信息;将 trace.txt 拖入 Chrome 的 Chrome Tracing(chrome://tracing)可可视化主线程时间线。

4.3 原因定位——故障树分析

根据 trace 中的现象组合,建立以下故障树,快速判别根因:

主线程 ANR 原因故障树 主线程 ANR Binder 调用阻塞 锁等待 CPU 竞争 / 调度延迟 同步 Binder 调用耗时 服务端线程池耗尽 主线程等锁(monitor) 死锁 / 锁持有时间过长 主线程持续 Runnable 系统 CPU 过载 / 中断 异步化 Binder 调用 锁粒度优化 / 避免死锁 线程池调优 / 限制前台负载 故障树:根节点 → 阻塞类型 → 子原因 → 修复方向

4.4 判断规则与排查表

根据 trace 状态字段迅速锁定类别:

主线程状态栈顶特征诊断结论优先级
BlockedBinderProxy.transact()Binder 同步调用阻塞,服务端可能慢或 Binder 线程池满高
BlockedObject.wait() / LockSupport.park()锁等待,其他线程持有锁未释放高
Runnable无阻塞函数,但累计 running > 5000msCPU 调度延迟,需分析系统负载和线程优先级中
WaitingObject.wait() + 无 held by条件等待,可能被 notify 延迟或死循环中
定位口诀:
阻塞看锁链,运行看负载;Binder 查线程,死锁找循环。

4.5 修复动作

三种典型场景的修复方向:

  1. Binder 调用阻塞:将导航相关的 Binder 调用改为异步(Oneway),或使用 AsyncTask / HandlerThread 切换到后台线程;若必须同步,缩短服务端处理时间(如缓存线路数据)。
  2. 锁等待:使用 synchronized 锁时减少临界区代码;避免在 onDraw / onLayout 等主线程回调中获取锁;改用 ReadWriteLock 或 ConcurrentHashMap 等无锁结构。
  3. CPU 竞争:减少主线程中的浮点计算、大量日志输出、不合理循环;将导航地图渲染的 CPU 密集操作移入 RenderThread 或使用 OpenGL。
// Binder 异步化示例(服务端 AIDL):
interface IRouteService {
    // 改为 oneway 方法
    oneway void fetchRouteAsync(in RouteRequest request);
}

4.6 复测

修复后使用以下方法验证:

  • 模拟导航操作重复 50 次,观察 adb logcat |grep ANR 不再出现主线程 ANR。
  • 利用 StrictMode 捕获主线程的 Binder 调用和锁持有时间:
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
        .detectDiskReads()
        .detectDiskWrites()
        .detectNetwork()
        .penaltyLog()
        .build());
  • 在 Dalvik Monitor 中检查锁竞争:adb shell dumpsys window monitors 应无长时间等待。

4.7 预防

  • 开发阶段:启用 StrictMode 的 penaltyDeath() 或 penaltyLog() 并集成到 CI。
  • 发布前:在低端设备(例如 2GB RAM 机型)上跑导航自动化测试,收集 trace。
  • 线上监控:接入 ANRWatchDog 或 Android Vitals,ANR 发生时自动上传 trace 和 CPU 快照。