主线程消息循环:Looper、Handler、MessageQueue
聊到ANR,绕不开这三个老伙计——Looper、Handler、MessageQueue。说白了,它们就是安卓主线程的“消息流水线”。我早年刚接触Android时,以为Handler就是个发消息的工具,后来踩了坑才明白,不理解消息循环,优化主线程就是空谈。
MessageQueue——消息的等待室
MessageQueue是个单向链表,按时间顺序把Message串起来。你想想看,主线程不可能同时处理所有事件,只能一个一个来。MessageQueue就像个缓冲区,把待处理的消息排好队。
它内部维护一个mMessages指针,指向队列头部。新消息插入时,会根据when(执行时间)找到合适位置。延迟消息就是靠这个时间戳实现的。
Looper——永不休息的轮询工
Looper负责从MessageQueue里不断取消息。每个线程只能有一个Looper,主线程的Looper在应用启动时就创建好了。它的核心就是一个死循环:
for (;;) {
Message msg = queue.next(); // 可能会阻塞
if (msg == null) return; // 退出条件
msg.target.dispatchMessage(msg);
msg.recycleUnchecked();
}
注意这个next()方法——如果队列为空,或者下一条消息还没到执行时间,线程就会在这里挂起(block)。这恰恰是ANR的温床:如果主线程在这个循环里被卡住超过5秒,系统就会判定ANR。
我记得有一次排查线上ANR,发现是一个高频率的postDelay循环导致next()返回值太慢。后来调整为批量处理,才把问题解决。
Handler——发送与处理的双面胶
Handler绑定到特定Looper(也就是特定线程)。它有两个角色:
- 发送端:通过
sendMessage或post把Message塞进目标线程的MessageQueue。 - 处理端:Looper取到消息后,调用
handleMessage回调执行。
说白了,Handler就是桥。你可以在子线程new一个Handler,指定主线程的Looper,这样子线程就能安全地把任务丢给主线程执行。
new Handler(Looper.getMainLooper())。避免用无参构造,因为Android R之后它被标记为deprecated了,而且容易引发行内存泄露。
主线程与子线程的通信机制
子线程怎么跟主线程说话?典型的做法就是通过Handler发送消息。流程如下:
- 主线程创建Handler(绑定主线程Looper)。
- 子线程调用
handler.sendMessage(msg)。 - Message被加入主线程的MessageQueue。
- 主线程Looper轮询到该消息,回调
handleMessage。
为什么必须这样?因为Android UI操作不是线程安全的。直接在其他线程更新UI会抛CalledFromWrongThreadException。我刚入行时就犯过这种错——在子线程改TextView,崩溃后还一脸懵。
工作流程图
下面这张图概括了整个消息循环的脉络。画的时候我特意把“阻塞点”标红了——ANR往往就发生在这里。
三者的职责分工
把它们拆开看更清晰:
| 组件 | 职责 | 常见误区 |
|---|---|---|
| MessageQueue | 存储消息,按时间排序 | 认为它可以主动消费消息 — 不,它只负责排队 |
| Looper | 死循环取消息,分发给Handler | 以为Looper可以自己处理消息 — 其实它只负责调度 |
| Handler | 发送消息 + 处理消息回调 | 忘记指定Looper导致线程绑定错乱 |
子线程发消息到主线程的典型写法
我推荐用这种方式,简洁且安全:
// 在主线程创建Handler
Handler mainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
// 这里就是在主线程执行了
updateUI(msg.obj);
}
};
// 子线程中发送消息
new Thread(() -> {
// 注意:这里不能直接操作UI
Message msg = Message.obtain();
msg.obj = "Hi, UI thread!";
mainHandler.sendMessage(msg);
}).start();
一个小细节:异步消息 vs 同步消息
MessageQueue其实区分“同步”和“异步”消息。异步消息可以越过同步屏障(Barrier)优先执行。这个机制被ViewRootImpl用来处理VSync信号。不过日常开发我们很少直接操作,知道有这么回事就行。
你想想看,如果所有消息都按顺序慢慢来,界面绘制就会被卡住。异步消息就是插队特权,保障渲染帧率。我在优化列表滑动卡顿的时候,曾经利用Barrier机制让布局计算优先于其他任务,效果很明显。