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.txtadb 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 / Perfettoatrace -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() 执行时间过长 → 细分:
    1. 界面重绘:onLayout / onDraw 中做了复杂计算、Bitmap 解码、IO
    2. 长时 Binder 调用:跨进程请求(如 ContentProvider query、系统服务调用)
    3. 锁竞争:主线程等待子线程释放锁,或等待子线程完成 Future.get()
    4. SharedPreferences commit:同步写磁盘导致 IO 阻塞
    5. 大文件读写:在主线程执行 InputStream.read() 或 SQLite 查询
    6. 死锁:主线程持锁 A 等待锁 B,而子线程持锁 B 等待锁 A
主线程消息循环与 Handler 调度流程 应用主线程启动 Looper.prepare() Looper.loop() 无限循环 (线程入口核心) MessageQueue.next() nativePollOnce() 等待唤醒 消息出队(Message) Handler.dispatchMessage() handleMessage / Runnable.run() ⚠ 耗时操作 / 锁 / IO 循环处理下一条消息 阻塞判断: nativePollOnce 超过 5 秒 常见阻塞原因: SharedPreferences.commit BitmapFactory.decodeStream

3.4 修复动作

根据故障树排查出的具体原因,选择对应的修复策略:

原因分类修复方案代码示例
耗时计算迁移到子线程,使用 HandlerThread 或 AsyncTask(已废弃建议 Kotlin Coroutines)
Thread(runnable).start() // 或 GlobalScope.launch(Dispatchers.IO) { ... }
SharedPreferences 同步写入改为异步 apply() 或使用 MMKV/DataStore
getSharedPreferences("prefs",0).edit().putString("key","val").apply()
Binder 长时间等待使用 ProgressCallback 或缓存结果,避免主线程同步等待
// 将 bindService 的回调放到主线程 Handler 中处理
死锁按固定顺序获取锁,或改用 ReentrantLock 设置超时
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) { ... }
Bitmap 解码使用 BitmapFactory.Options.inSampleSize 先缩放在子线程解码
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }

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。
复测清单:
  1. traces.txt 中主线程堆栈不再指向 nativePollOnce 超过 5 秒。
  2. logcat 中无新 ANR 记录。
  3. Perfetto 中主线程 CPU 利用率低于 50%,且无连续 3 帧丢帧。

3.6 预防机制

从代码层面主动拦截主线程阻塞:

工具/方法作用配置方式
StrictMode检测主线程磁盘 IO、网络、慢 SQL、Object 泄露
if (BuildConfig.DEBUG) {
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
        .detectAll()
        .penaltyLog()
        .penaltyFlashScreen()
        .build());
}
BlockCanary监控主线程消息处理时长,超过阈值(默认 2 秒)自动上报
BlockCanary.install(this, new AppBlockCanaryContext()).start();
自定义 Looper 日志修改 Looper.loop() 中的 Printer,统计每条消息耗时
Looper.getMainLooper().setMessageLogging(new Printer() {
    @Override public void println(String x) {
        // 解析 x 中的 ">>>>> Dispatching to Handler"
        // 记录开始/结束时间差
    }
});
线程池隔离将网络请求、DB 查询放在专用线程池,避免与 UI 线程混用
ExecutorService executor = Executors.newFixedThreadPool(4);
团队规约: Code Review 时,对所有在主线程中出现的 new FileInputStream()、SharedPreferences.edit().commit()、BitmapFactory.decodeResource() 打上高危标记,强制使用异步或离线方案。