3. 主线程阻塞识别:主线消息队列、Looper、Handler调度异常
3.1 现象识别
当应用发生主线程阻塞型 ANR 时,用户会感知到界面卡住无响应,系统弹出“应用无响应”对话框。从 logcat 中可以识别到以下特征日志:
Input dispatching timed out (Waiting to be non-null...)—— 输入事件等待超时Broadcast of Intent ... timed out—— 广播超时Service ... timed out—— 服务超时Waiting for main thread—— 其他线程等待主线程锁
关键判断规则: 在
traces.txt 中找到名为 "main"(或 "ActivityManager")的线程堆栈,若其调用栈深度超过 20 层,且长时间停留在 android.os.MessageQueue.next() 或 android.os.Handler.dispatchMessage() 附近,则极可能是主线程消息队列调度异常。
3.2 数据采集
采集主线程阻塞证据需要以下数据源:
| 数据源 | 命令/方法 | 关键字段 |
|---|---|---|
| traces.txt | adb pull /data/anr/traces.txt 或 am trace-ipc | "main" prio=5 tid=1 后的堆栈 |
| logcat 缓冲区 | adb logcat -b events -b main -b system | grep -E "ANR|timed out|Watchdog" | ANR 类型、包名、耗时 |
| Systrace / Perfetto | atrace -z --async_start -b 20480 gfx input view | 主线程 vsync 帧间隔、Input 分派时间线 |
| Thread dump 快照 | adb shell kill -3 <pid> 或从 dumpsys dropbox 提取 | 锁竞争、Binder 等待队列 |
采样时刻: ANR 触发后 5 秒内的 trace 文件最为准确。若需要复现,可用
adb shell monkey --throttle 200 --pct-hang 80 100 模拟高负载。
3.3 原因定位 —— 故障树分析
主线程阻塞故障树(Main Thread Blocking Fault Tree)
根节点:ANR 发生时主线程处于阻塞状态 → 二选一分支:消息队列挂起 或 Handler 调度异常
- 消息队列挂起 → MessageQueue.next() 阻塞在 nativePollOnce() → 可能原因:
- 没有新消息(IdleHandler 长时间执行)
- 队列头部消息是 Barrier(同步屏障),且没有异步消息触发
- Looper 被 quit / quitSafely 停止
- Handler 调度异常 → dispatchMessage() 执行时间过长 → 细分:
- 界面重绘:onLayout / onDraw 中做了复杂计算、Bitmap 解码、IO
- 长时 Binder 调用:跨进程请求(如 ContentProvider query、系统服务调用)
- 锁竞争:主线程等待子线程释放锁,或等待子线程完成 Future.get()
- SharedPreferences commit:同步写磁盘导致 IO 阻塞
- 大文件读写:在主线程执行 InputStream.read() 或 SQLite 查询
- 死锁:主线程持锁 A 等待锁 B,而子线程持锁 B 等待锁 A
3.4 修复动作
根据故障树排查出的具体原因,选择对应的修复策略:
| 原因分类 | 修复方案 | 代码示例 |
|---|---|---|
| 耗时计算 | 迁移到子线程,使用 HandlerThread 或 AsyncTask(已废弃建议 Kotlin Coroutines) |
|
| SharedPreferences 同步写入 | 改为异步 apply() 或使用 MMKV/DataStore |
|
| Binder 长时间等待 | 使用 ProgressCallback 或缓存结果,避免主线程同步等待 |
|
| 死锁 | 按固定顺序获取锁,或改用 ReentrantLock 设置超时 |
|
| Bitmap 解码 | 使用 BitmapFactory.Options.inSampleSize 先缩放在子线程解码 |
|
3.5 复测方法
- 静态验证: 确认修改后的代码中主线程没有长时间的
for循环、Thread.sleep()、InputStream.read()调用。 - 动态验证: 使用
adb shell am broadcast -a android.intent.action.TIME_TICK或 Monkey 测试模拟高并发事件,配合systrace --trace=view,gfx,input --time=10观察主线程帧间隔是否稳定在 16ms 以内。 - 压力验证: 在低端设备(4 核以下、2GB 内存)上反复执行触发场景,确保不再复现 ANR。
复测清单:
- traces.txt 中主线程堆栈不再指向 nativePollOnce 超过 5 秒。
- logcat 中无新 ANR 记录。
- Perfetto 中主线程 CPU 利用率低于 50%,且无连续 3 帧丢帧。
3.6 预防机制
从代码层面主动拦截主线程阻塞:
| 工具/方法 | 作用 | 配置方式 |
|---|---|---|
| StrictMode | 检测主线程磁盘 IO、网络、慢 SQL、Object 泄露 | |
| BlockCanary | 监控主线程消息处理时长,超过阈值(默认 2 秒)自动上报 | |
| 自定义 Looper 日志 | 修改 Looper.loop() 中的 Printer,统计每条消息耗时 | |
| 线程池隔离 | 将网络请求、DB 查询放在专用线程池,避免与 UI 线程混用 | |
团队规约: Code Review 时,对所有在主线程中出现的
new FileInputStream()、SharedPreferences.edit().commit()、BitmapFactory.decodeResource() 打上高危标记,强制使用异步或离线方案。