4. ANR分析与优化:ANR触发条件、trace文件解读、锁竞争消除

4.1 ANR触发条件与分类

ANR(Application Not Responding)是Android系统检测到主线程(UI线程)阻塞超过阈值后触发的用户可见提示。不同场景的阈值如下:

场景超时时间触发点
输入事件(按键/触摸)5秒InputDispatcher 超时
BroadcastReceiver(前台)10秒BroadcastQueue 处理超时
BroadcastReceiver(后台)60秒BroadcastQueue 处理超时
Service(前台)20秒ActiveServices 超时
Service(后台)200秒ActiveServices 超时
ContentProvider10秒Provider 发布超时
关键阈值调整思路:线上ANR治理中,可通过 WindowManagerService 调整输入事件响应超时,或通过 ActivityManagerService 的配置覆盖阈值。但注意:延长阈值仅治标,应优先定位锁竞争。

4.2 ANR的完整触发流程

当主线程阻塞时,AMS会经历以下阶段:

  1. 调度器通知:InputDispatcher 或 BroadcastQueue 等调度器检测到消息处理超时。
  2. AMS注入ANR:AMS 调用 appNotResponding() 方法,记录进程状态。
  3. 生成Trace:系统向目标进程发送 SIGQUIT 信号,进程内部 dump 所有线程堆栈到 /data/anr/traces.txt。
  4. 弹ANR对话框:如果进程仍在前台,显示“应用无响应”对话框。
  5. 写入EventLog:记录 am_anr 和 am_crash 相关日志。
关键源码入口: ActivityManagerService.java 中的 appNotResponding() 方法,以及 ProcessRecord.java 中的进程状态管理。

4.3 Trace文件解读实战

Trace文件是ANR分析的第一手证据。典型的 trace 文件结构如下:

----- pid 12345 at 2025-03-15 10:30:45 -----
Cmd line: com.example.myapp
Build fingerprint: 'google/redfin/redfin:12/SP1A.210812.016/12345678:user/release-keys'
ABI: 'arm64'

DALVIK THREADS (12):
"main" prio=5 tid=1 Runnable
  | group="main" sCount=0 dsCount=0 flags=0 obj=0x71c2f0e0 self=0x7a9a6c0000
  | sysTid=12345 nice=-10 cgrp=default sched=0/0 handle=0x7a9a6c0000
  | state=R schedstat=( 123456789 0 123 ) utm=5 stm=0 core=0 HZ=100
  | stack=0x7fdf000000-0x7fdf020000 stackSize=8MB
  | held mutexes= "mutex1" "mutex2"
  native: #00 pc 0000000000  /system/lib64/libc.so (syscall+28)
  native: #01 pc 0000000000  /system/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks+148)
  native: #02 pc 0000000000  /system/lib64/libart.so (art::JNI::GetStringUTFChars+???)
  at com.example.MyActivity.doSomething(MyActivity.java:45)
  at android.os.Handler.handleCallback(Handler.java:938)
  at android.os.Handler.dispatchMessage(Handler.java:99)
  at android.os.Looper.loop(Looper.java:237)
  at android.app.ActivityThread.main(ActivityThread.java:8075)
  at java.lang.reflect.Method.invoke(Native method)

"Binder:12345_1" prio=5 tid=8 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x12c60190 self=0x7a9a700000
  | sysTid=12346 nice=0 cgrp=default sched=0/0 handle=0x7a9a700000
  | state=S schedstat=( 0 0 0 ) utm=0 stm=0 core=0 HZ=100
  | stack=0x7f6c000000-0x7f6c020000 stackSize=1MB
  native: #00 pc 0000000000  /system/lib64/libc.so (__ioctl+4)
  native: #01 pc 0000000000  /system/lib64/libbinder.so (???)
  at android.os.BinderProxy.transactNative(Native method)
  at android.os.BinderProxy.transact(BinderProxy.java:571)
  at com.example.IRemoteService$Stub$Proxy.doRemoteCall(IRemoteService.java:123)
  ... wait for main thread response ...
Trace解读要点:
● 重点关注 "main" 线程的 held mutexes 字段——它列出了当前持有的锁。
● 如果 main 线程处于 Blocked 或 Waiting 状态,看它具体等待哪一把锁(通常是 contending with 后面跟的线程名称)。
● 检查是否有 WAITING 状态的 Binder 线程,它们可能正在等待主线程响应。

4.4 锁竞争分析与消除

最常见的ANR元凶是主线程与后台线程竞争同一把锁。以下是系统级和自定义级锁竞争场景的排查方法:

锁类型典型表现Trace特征
ActivityManagerService 全局锁主线程调用 AMS 方法时阻塞"held mutexes=monitor" "contending with thread id X"
WindowManagerService 锁动画或布局时卡住WMS 相关调用栈,如 relayoutWindow
ContentProvider 锁查询数据库时主线程等待CP 相关 query 调用,以及 Binder 线程等待返回
自定义对象锁synchronized 或 ReentrantLock 争用线程栈上有 wait() 或 park()

4.4.1 消除AMS全局锁竞争

常见优化手段:

  • 将耗时操作移至工作线程,避免在主线程中调用 bindService、startActivityForResult 等阻塞型AMS API。
  • 使用 AsyncTask 或 HandlerThread 异步处理AMS回调。
  • 对于 BroadcastReceiver,在 onReceive() 中启动 IntentService 或 JobIntentService 避免广播超时。

4.4.2 WMS锁竞争消除

WMS锁(mWindowMap/WindowManagerService.this)常被主线程和非主线程同时访问:

  • 使用 WindowManagerGlobal 的同步方法时,确保不持有其他锁再请求WMS锁(避免死锁)。
  • 在 View.draw() 中不要执行耗时操作,因为渲染流程会在WMS锁下执行。

4.4.3 自定义锁消除策略

针对应用层代码,推荐以下工具与方法:

  • 使用 Systrace 和 Method Tracing 捕获锁持有时间。
  • 用 StrictMode 检测主线程磁盘IO、网络访问等隐含锁竞争。
  • 将 synchronized 块拆分为细粒度锁,或使用 ReadWriteLock 替代。
  • 对于高频访问的共享数据,考虑使用 ConcurrentHashMap 或 AtomicReference 无锁方案。
实战案例:某直播应用频繁ANR,trace显示 "main" 线程 held mutexes=mutex1,而线程 "worker-1" 也在等待 mutex1。经排查,doSomething() 内部使用了 synchronized 保护全局对象,且该同步块中执行了图片解码。优化方案:将图片解码移至线程,仅使用原子变量控制状态,ANR归零。

4.5 ANR排查与调试命令

以下是分析ANR时最常用的命令集合:

# 查看系统当前ANR trace文件(需要root或系统权限)
adb shell cat /data/anr/traces.txt

# 若文件过大,只取最近一次ANR
adb shell ls -lt /data/anr/ | head -5
adb shell cat /data/anr/traces.txt | grep -A 30 "^----- pid"

# 导出到本地分析
adb pull /data/anr/traces.txt ./traces.txt

# 实时监控ANR日志(不需要等待对话框出现)
adb logcat -b events | grep -E "am_anr|am_crash"

# 使用systrace捕捉锁竞争
python systrace.py -t 10 -o trace.html sched freq idle am wm

# 导出当前进程所有线程堆栈(无需ANR)
adb shell kill -3 

4.6 锁竞争消除的完整流程图

ANR分析与锁竞争消除流程 主线程阻塞触发ANR AMS 注入SIGQUIT 解读Trace文件 锁竞争? 其他原因排查
IO/Draw/GC 定位锁类型 AMS/WMS锁 / 自定义锁 异步化 / 细粒度锁 验证ANR消失 否 是 Trace文件需关注 held mutexes、 contending with、stack 调用链

4.7 常见错误与避坑指南

  • 错误1:误判ANR来源。 同一trace中有多个线程阻塞,但只有主线程空导致ANR。应优先分析主线程栈,再查看哪些线程持有主线程等待的锁。
  • 错误2:盲目延长超时阈值。 在 system_server 中修改 KEY_DISPATCHING_TIMEOUT 或添加 config_anrTimeout 只能掩盖问题,锁竞争依然存在,还会导致后台CPU持续升温。
  • 错误3:忽略Binder线程栈。 许多ANR并非主线程直接阻塞,而是主线程在等待Binder IPC返回,而Binder线程又被其他事务阻塞。此时需检查Binder线程的等待链。
  • 错误4:锁消除时引入死锁。 更改锁粒度或引入读写锁后,需要用 ThreadDump 和死锁检测工具验证,防止新引入循环等待。

4.8 性能与稳定性优化总结

ANR优化核心目标:确保主线程在5秒内能够响应用户事件。以下为量化检查清单:

优化项优化手段预期效果
AMS锁竞争主线程避免直接调用AMS阻塞方法;使用 BroadcastReceiver + goAsync()减少主线程等待AMS锁时间 > 80%
WMS锁竞争减少在动画期间调用 requestLayout();使用 Choreographer 合并帧操作布局次数降低 > 50%
自定义锁竞争使用 ConcurrentLinkedQueue 替代 synchronized(ArrayList);ReadWriteLock 读多写少场景主线程锁等待时间 < 50ms
主线程IO严格使用 AsyncTask / 线程池执行文件读写主线程IO耗时归零
主线程GC减少短生命周期对象分配;使用 SparseArray 替代 HashMapGC pause 平均 < 5ms
推荐工具链:
● systrace.py -t 10 -o trace.html sched freq idle am wm gfx input view
● simpleperf record -g -e cpu-clock -p <pid> --duration 10
● Android Studio CPU Profiler 中的 "View Trace" 选项,可直接解析 traces.txt 并提供可视化锁竞争分析。