主线程阻塞原理:Looper 与 Handler 机制,常见阻塞原因如何导致 ANR
这一章我们来啃一块硬骨头——主线程的 Looper 和 Handler 到底怎么工作的,为什么它们一“卡住”就会变成 ANR。我在 ExoPlayer 的初始化场景里踩过不少坑,很多播放器启动超时的案例,根因就是主线程被耗时的 IO 或锁给堵死了。
从 Looper 说起:主线程的消息循环
Android 应用的 main 线程(也就是 UI 线程),启动时系统会帮你创建一个 Looper。这个 Looper 内部维护了一个消息队列(MessageQueue),然后不停地调用 Looper.loop() 从队列里取出消息分发给对应的 Handler。说白了,这就是一个“死循环”——取消息、处理消息、再取消息……
我记得早期在做短视频 SDK 时,发现某个定制 ROM 上 ExoPlayer 频繁触发 ANR。最后定位到是 Looper.loop() 被阻塞了,因为主线程在加载媒体元数据时做了大量文件 IO。嗯,这就是我们下面要聊的根源。
Handler 的 post 与 dispatchMessage
Handler 的工作流程很简单:post(Runnable) 将 Runnable 封装成 Message 塞进 MessageQueue;Looper 取出后在目标线程上调用 dispatchMessage(),最终执行 handleMessage 或直接 run。那它为什么会导致 ANR?
- 主线程 Handler 收到耗时任务——比如你在主线程的 Handler 里做了数据库查询、网络请求、大文件读写。
- 消息队列被阻塞——一个慢消息会堵住后面所有的 UI 更新、触摸事件、绘制请求。
我曾经见过一个案例:开发者在 onCreate 中用主线程 Handler 去解析一个 10MB 的 JSON,结果用户一启动就看到黑屏 3 秒,紧接着 ANR 弹窗。说白了,Handler 本身不背锅,是放进去的任务太重了。
主线程阻塞的三大元凶
根据我排查几十个 ANR 的经验,主线程阻塞无非就这三大类:
| 类型 | 典型场景 | 危害程度 |
|---|---|---|
| IO 操作 | 读文件、写数据库、网络请求(OKHttp 同步调用) | ★★★★★ |
| 锁竞争 | 主线程持锁等待子线程、死锁、长时间同步代码块 | ★★★★☆ |
| 复杂计算 | Bitmap 处理、JSON 解析、正则匹配、大型循环 | ★★★☆☆ |
IO 操作
在项目中,我最常碰到的是 SharedPreferences 的 apply() 和 commit() 混用,导致主线程等待磁盘写入完成。还有 ExoPlayer 初始化时,如果 MediaSource 的创建过程里包含了读 manifest 文件的操作(比如本地 HLS 索引),一旦文件较大或者存储慢,主线程就会被卡住。
FileInputStream.read(),导致 3-5 秒的 IO 阻塞。解决方法是把所有文件读取放到子线程,通过 Callback 回到主线程更新状态。
锁竞争
主线程拿锁等待其他线程释放——这是最隐蔽的问题。比如 ExoPlayer 内部某些组件(如 MediaCodec、TrackSelector)可能使用了同步块,如果子线程长时间持有锁,主线程去调用同锁保护的接口就会死等。我记得有一次 ANR 日志显示主线程在 MediaCodec.configure() 里卡住,一看日志是另一个线程在做 release() 时持锁超过了 4 秒。
复杂计算
严格来说,计算密集型任务导致 ANR 的概率比前两者低,但依然常见。比如在主线程 Handler 里直接解码一张 4K 图片,或者执行 String.matches() 带灾难性回溯的正则。我在优化一个相册 App 时,发现用户快速滑动时 ANR,正是因为在主线程做 File.listFiles()(其实也属于 IO 和计算的混合)。
这些阻塞怎么变成 ANR 的?
系统监测 ANR 的机制其实很“粗暴”。它给每个消息处理设置了一个超时计时器:
- 对于触摸事件和按键事件:
InputDispatching Timeout—— 5 秒内没有响应。 - 对于广播接收器(BroadcastReceiver):前台 10 秒 / 后台 60 秒。
- 对于 Service 的
onStartCommand:20 秒。
说白了,只要主线程的 Looper 在某个消息上卡了足够久,系统就会收到“心跳”超时信号,直接弹出 ANR 对话框。下面这张图可以帮你直观理解整个链条:
从这张图你能看明白:只要消息处理超时,系统就会直接叫停。而且不仅是 Input 事件,BroadcastReceiver.onReceive() 和 Service.onStartCommand() 的超时机制也类似,只不过阈值不同。
main (native) 附近的调用栈。如果看到 Looper.loop() -> Handler.dispatchMessage() 卡在某一行,那基本上就是三种阻塞中的一种。比如说,卡在 FileInputStream.read() 就是 IO,卡在 Object.wait() 就是锁竞争。
一个小结
主线程阻塞的核心就一句话:Looper 的消息处理不能慢,一慢就 ANR。常见的三大原因——IO、锁、计算,基本都是开发者不小心把耗时操作放到了主线程里。你需要记住:Handler 只是工具,真正引发问题的是你塞进去的任务。