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 超时 |
| ContentProvider | 10秒 | Provider 发布超时 |
关键阈值调整思路:线上ANR治理中,可通过
WindowManagerService 调整输入事件响应超时,或通过 ActivityManagerService 的配置覆盖阈值。但注意:延长阈值仅治标,应优先定位锁竞争。
4.2 ANR的完整触发流程
当主线程阻塞时,AMS会经历以下阶段:
- 调度器通知:InputDispatcher 或 BroadcastQueue 等调度器检测到消息处理超时。
- AMS注入ANR:AMS 调用
appNotResponding()方法,记录进程状态。 - 生成Trace:系统向目标进程发送 SIGQUIT 信号,进程内部 dump 所有线程堆栈到
/data/anr/traces.txt。 - 弹ANR对话框:如果进程仍在前台,显示“应用无响应”对话框。
- 写入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" 线程的
● 如果 main 线程处于
● 检查是否有
● 重点关注 "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 锁竞争消除的完整流程图
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 替代 HashMap | GC pause 平均 < 5ms |
推荐工具链:
●
●
● Android Studio CPU Profiler 中的 "View Trace" 选项,可直接解析
●
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 并提供可视化锁竞争分析。