主线程消息循环:Looper、Handler、MessageQueue

聊到ANR,绕不开这三个老伙计——Looper、Handler、MessageQueue。说白了,它们就是安卓主线程的“消息流水线”。我早年刚接触Android时,以为Handler就是个发消息的工具,后来踩了坑才明白,不理解消息循环,优化主线程就是空谈。

MessageQueue——消息的等待室

MessageQueue是个单向链表,按时间顺序把Message串起来。你想想看,主线程不可能同时处理所有事件,只能一个一个来。MessageQueue就像个缓冲区,把待处理的消息排好队。

它内部维护一个mMessages指针,指向队列头部。新消息插入时,会根据when(执行时间)找到合适位置。延迟消息就是靠这个时间戳实现的。

关键点:MessageQueue本身不干活,它只管存。真正的消费者是Looper。

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,这样子线程就能安全地把任务丢给主线程执行。

我的习惯:构造Handler时显式传入Looper——new Handler(Looper.getMainLooper())。避免用无参构造,因为Android R之后它被标记为deprecated了,而且容易引发行内存泄露。

主线程与子线程的通信机制

子线程怎么跟主线程说话?典型的做法就是通过Handler发送消息。流程如下:

  1. 主线程创建Handler(绑定主线程Looper)。
  2. 子线程调用handler.sendMessage(msg)。
  3. Message被加入主线程的MessageQueue。
  4. 主线程Looper轮询到该消息,回调handleMessage。

为什么必须这样?因为Android UI操作不是线程安全的。直接在其他线程更新UI会抛CalledFromWrongThreadException。我刚入行时就犯过这种错——在子线程改TextView,崩溃后还一脸懵。

工作流程图

下面这张图概括了整个消息循环的脉络。画的时候我特意把“阻塞点”标红了——ANR往往就发生在这里。

主线程消息循环工作流 子线程 (Handler.sendMessage) MessageQueue (消息队列) Looper.next() 可能阻塞 有消息? 否 循环继续 是 dispatchMessage handleMessage recycleUnchecked 📌 注意: Looper.next()阻塞是主线程唯一的合法休眠点。如果在该点之外执行耗时操作, 系统将触发ANR。所有UI更新必须经过Handler发往主线程消息队列。

三者的职责分工

把它们拆开看更清晰:

组件 职责 常见误区
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();
我曾经踩过的坑:在子线程里直接new Handler(),没有传入Looper。结果这个Handler绑定到了子线程的Looper(如果子线程调用了Looper.prepare())。然后我发消息想更新UI,消息被发到了子线程的消息队列,handleMessage在子线程执行,直接崩溃。教训就是:永远明确Handler绑定的Looper。

一个小细节:异步消息 vs 同步消息

MessageQueue其实区分“同步”和“异步”消息。异步消息可以越过同步屏障(Barrier)优先执行。这个机制被ViewRootImpl用来处理VSync信号。不过日常开发我们很少直接操作,知道有这么回事就行。

你想想看,如果所有消息都按顺序慢慢来,界面绘制就会被卡住。异步消息就是插队特权,保障渲染帧率。我在优化列表滑动卡顿的时候,曾经利用Barrier机制让布局计算优先于其他任务,效果很明显。