第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.txtANR日志、SystemServer警告、WindowManager超时
Dumpsysadb shell dumpsys activity processes进程状态、binder事务、等待锁信息
Systracepython systrace.py -t 10 -o trace.html主线程时间切片、CPU调度、方法耗时概况
提醒:生产环境很难复现,可在测试阶段开启 “不保留活动” 并配合 StrictMode 提前捕获,避免线上ANR全量采集。

1.3 原因定位——主线程消息循环模型

Android的主线程(UI线程)运行在一个 Looper 驱动的消息循环中。所有输入事件、四大组件回调、`View.draw()` 等都被封装成 Message 或 Runnable 排队执行。当其中一个消息执行时间超过5秒,且没有新的输入事件被处理,系统即触发ANR。

主线程消息循环与ANR触发路径 用户输入 / 触摸事件 MessageQueue 队列 Looper.dispatchMessage IO 阻塞
(磁盘读/网络) 锁竞争
(synchronized/wait)
长计算 / GC
(10ms+ loop)
System 检测5秒超时 traces.txt + ANR 弹窗 (SIGNAL_QUIT) 常见阻塞类型: IO · 锁 · 计算 · Binder

常见的阻塞类型可分为三类,下表从堆栈特征和出发命令角度进行对比:

阻塞类型典型堆栈顶定位关键词常见场景
IO 阻塞java.io.FileInputStream.read() / OkHttp RealCallread、write、await主线程直接读取文件 / 网络请求阻塞
锁竞争java.lang.Object.wait() / ReentrantLock.lock()wait、LockSupport.park主线程等子线程释放锁;SharedPreferences提交
长计算/GCdalvik.system.VMRuntime.allocate() / Thread.sleep()allocate、sleep、yieldBitmap 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 复测与预防

修复完成后需从三个层面验证:

  1. 压力测试:使用 adb shell monkey --throttle 100 -p com.example.app 50000 模拟大量事件冲刷主线程,同时 dumpsys activity processes | grep "anr" 观察ANR计数。
  2. Systrace 分析:抓取10秒场景,检查主线程 帧渲染 及 Looper 周期 是否有超过100ms的连续阻塞。
  3. StrictMode 全面开启:在Debug构建中启用磁盘和网络检查,线上可通过 StrictMode.VmPolicy 输出违规日志。
预防原则:主线程只能做三件事——接收事件、更新UI、以及轻量级状态变更。任何超过1ms的工作都必须委托到后台线程。通过 HandlerThread 或 Coroutine 的 Dispatchers.Main 调度回主线程更新UI,形成闭环。