第1章 ANR的本质与主线程阻塞模型
1.1 现象识别
当主线程被长时间阻塞(通常超过5秒),系统会判定为“应用无响应”(ANR)。用户端表现为:触摸、按键无反馈,界面冻结,随后弹出系统级ANR弹窗(如“应用 已无响应”),提供“等待”或“关闭”选项。
在 logcat 中会输出关键含“ANR”的日志行,例如:
I/WindowManager: ANR in com.example.app (com.example.app/.MainActivity)
PID: 12345
Reason: Input dispatching timed out (Waiting to send key event because the touched window has not finished processing certain input events that were delivered to it)
同时,系统会在 /data/anr/traces.txt 中保存ANR时刻所有线程的堆栈,这是最直接的证据。若主线程堆栈显示处于 Thread.sleep()、InputStream.read() 或 BinderProxy.transact() 等调用上,即可快速判定阻塞点。
要点:不要只依赖Java堆栈,
traces.txt 还可能包含Native线程信息,排查Binder、SurfaceFlinger时会用到。
1.2 数据采集
面对ANR,第一要务是保留现场。以下是标准采集清单:
| 数据源 | 命令/文件路径 | 关键内容 |
|---|---|---|
| ANR堆栈文件 | /data/anr/traces.txt | 所有线程堆栈、CPU使用率、加载类数 |
| Logcat(缓冲) | adb logcat -b all -d > logcat.txt | ANR日志、SystemServer警告、WindowManager超时 |
| Dumpsys | adb shell dumpsys activity processes | 进程状态、binder事务、等待锁信息 |
| Systrace | python systrace.py -t 10 -o trace.html | 主线程时间切片、CPU调度、方法耗时概况 |
提醒:生产环境很难复现,可在测试阶段开启 “不保留活动” 并配合
StrictMode 提前捕获,避免线上ANR全量采集。
1.3 原因定位——主线程消息循环模型
Android的主线程(UI线程)运行在一个 Looper 驱动的消息循环中。所有输入事件、四大组件回调、`View.draw()` 等都被封装成 Message 或 Runnable 排队执行。当其中一个消息执行时间超过5秒,且没有新的输入事件被处理,系统即触发ANR。
常见的阻塞类型可分为三类,下表从堆栈特征和出发命令角度进行对比:
| 阻塞类型 | 典型堆栈顶 | 定位关键词 | 常见场景 |
|---|---|---|---|
| IO 阻塞 | java.io.FileInputStream.read() / OkHttp RealCall | read、write、await | 主线程直接读取文件 / 网络请求阻塞 |
| 锁竞争 | java.lang.Object.wait() / ReentrantLock.lock() | wait、LockSupport.park | 主线程等子线程释放锁;SharedPreferences提交 |
| 长计算/GC | dalvik.system.VMRuntime.allocate() / Thread.sleep() | allocate、sleep、yield | Bitmap decode、JSON解析、循环写入Log |
| Binder卡顿 | android.os.BinderProxy.transact() | transact、waitForResponse | 跨进程调用被系统服务或远端卡住 |
1.4 修复动作
根据阻塞类型采取精确修复:
- IO阻塞:将所有磁盘/网络操作放入独立线程池,使用
AsyncTask、ThreadPoolExecutor或协程Dispatchers.IO。 - 锁竞争:使用
StrictMode检测主线程的写锁调用;对于SharedPreferences,使用apply()替代commit(),或迁移至DataStore。 - 长计算:拆分大型工作为小批次;Bitmap使用
BitmapFactory.Options.inSampleSize降低分辨率;GC问题可通过降低后台内存分配频率缓解。 - Binder卡顿:避免主线程串行调用
Binder服务;对不可控的外部服务设置超时(ContentResolver.query()使用CancellationSignal)。
1.5 复测与预防
修复完成后需从三个层面验证:
- 压力测试:使用
adb shell monkey --throttle 100 -p com.example.app 50000模拟大量事件冲刷主线程,同时dumpsys activity processes | grep "anr"观察ANR计数。 - Systrace 分析:抓取10秒场景,检查主线程 帧渲染 及 Looper 周期 是否有超过100ms的连续阻塞。
- StrictMode 全面开启:在Debug构建中启用磁盘和网络检查,线上可通过
StrictMode.VmPolicy输出违规日志。
预防原则:主线程只能做三件事——接收事件、更新UI、以及轻量级状态变更。任何超过1ms的工作都必须委托到后台线程。通过
HandlerThread 或 Coroutine 的 Dispatchers.Main 调度回主线程更新UI,形成闭环。