Handler(一):post 之后,Runnable 到底在哪个线程执行?

0 阅读9分钟

Handler(一):post 之后,Runnable 到底在哪个线程执行?

很多人第一次学 Handler,会把它记成一句话:

Handler 可以切线程。

比如子线程里写:

mainHandler.post(() -> updateUi());

最后 updateUi() 确实在主线程执行。这个现象没错,但“Handler 切线程”这个说法太粗了。它会让人以为 post() 像某种魔法一样,把当前调用栈直接切到另一条线程继续跑。

真实发生的事情不是这样。

调用 post 的线程:
    只负责把 Runnable 包装成 Message,并把 Message 放进目标 MessageQueue。
​
目标 Looper 所在线程:
    自己在 Looper.loop() 里取出这条 Message,然后执行 Runnable.run()。

所以这篇只回答一个问题:

handler.post(runnable) 之后,Runnable 为什么会在目标线程执行?

先把答案放在最前面:

Handler 不是线程,也不是 CPU 调度和代码运行的主体。
Handler 提供消息投递与分发逻辑,并持有目标 Looper 引用。
Looper 在创建时与当前 Thread 绑定。
post 只是入队。
目标 Thread 在 Looper.loop() 中调用 Handler.dispatchMessage(),
最终由这条 Thread 承载 Runnable、Callback 和 handleMessage 的执行。

本文源码以 Android 16 QPR2 的 android-16.0.0_r4 tag 为参考,正文只讨论 Handler 的稳定机制,不展开版本实现差异。

image.png

整条最小链路是:

调用线程
  -> Handler.post(Runnable)
  -> Runnable 包装成 Message.callback
  -> Handler.sendMessageAtTime()
  -> Handler.enqueueMessage()
  -> msg.target = 当前 Handler
  -> MessageQueue.enqueueMessage()
​
目标线程
  -> Looper.loop()
  -> MessageQueue.next()
  -> msg.target.dispatchMessage(msg)
  -> Handler.dispatchMessage()
  -> Runnable.run()

这条链路里最重要的分界是:

调用线程只负责入队;
目标 Looper 线程负责取出并执行。

后面所有源码都围绕这个分界展开。

一、Handler 是线程吗?

不是。

Handler 自己不创建线程,也不让线程启动。它只是保存了几个和消息投递有关的对象:

mLooper:这个 Handler 持有的 Looper 引用。
mQueue:这个 Looper 背后的 MessageQueue。
mCallback:构造 Handler 时传入的回调。
mAsynchronous:这个 Handler 发出的消息是否默认异步。

换成更直白的话:

从发送侧看:
    Handler 是向目标 MessageQueue 投递任务的入口。
​
从执行侧看:
    Message 被 Looper 取出后,
    Handler 又负责通过 dispatchMessage 选择具体回调路径。
​
真正承载这些代码执行的,仍然是目标 Thread。

所以判断一个 Runnable 最后在哪条线程执行,不能看 post() 在哪条线程被调用,而要看这个 Handler 持有哪个 Looper,以及该 Looper 与哪条 Thread 绑定。

最小例子:

Handler mainHandler = new Handler(Looper.getMainLooper());
​
new Thread(() -> {
    mainHandler.post(() -> {
        // 这里不是子线程。
        // 这里会在 MainLooper 所在线程执行。
    });
}).start();

这里有两个线程:

子线程:
    调用 mainHandler.post()。
​
主线程:
    跑 MainLooper.loop(),最后执行 Runnable.run()。

Handler 只决定“投到哪里”。它不决定“谁调用 post”,也不直接调度 CPU。

二、Handler 到底持有什么?

从对象关系看,Handler 持有的是 Looper 引用,不是直接持有 Thread。运行时,这个 Looper 与创建它的 Thread 绑定。

Android 源码里创建 Handler 时,核心逻辑可以抽成这样:

public Handler(Looper looper) {
    mLooper = looper;
    mQueue = looper.mQueue;
}

源码入口:

为什么现在更推荐显式传 Looper?因为隐式构造会从当前线程拿 Looper.myLooper()

Handler handler = new Handler();

这行代码看起来短,但它依赖当前线程已经调用过 Looper.prepare()。如果当前线程没有 Looper,会崩;如果当前线程有 Looper,但不是你以为的那条线程,消息就会被投到错误的队列里。

更稳的写法是:

Handler mainHandler = new Handler(Looper.getMainLooper());
Handler workerHandler = new Handler(handlerThread.getLooper());

这样一眼能看出任务会投向哪个 Looper。

这里要记住一个判断公式:

Handler 持有哪个 Looper;
这个 Looper 创建时绑定到哪条 Thread;
回调就由哪条 Thread 承载执行。

三、Looper 与 Thread 是什么关系?

更准确的说法是:Looper 在创建时与当前 Thread 绑定。

Looper.prepare() 会在当前线程创建一个 Looper,并把它放进 ThreadLocal。这意味着每条线程最多通过 Looper.myLooper() 查询到一个与自己绑定的 Looper。

源码逻辑骨架:

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<>();
​
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}
​
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

源码入口:

这里要避免把 ThreadLocal 理解成“其他线程永远拿不到这个 Looper 对象”。更准确的边界是:

其他线程调用 Looper.myLooper():
    只能得到自己线程绑定的 Looper,可能为 null。
​
但其他线程仍然可以通过:
    Looper.getMainLooper();
    HandlerThread.getLooper();
    或已有对象引用,
持有目标线程的 Looper,并向它投递任务。

ThreadLocal 限制的是“当前线程如何查询自己的 Looper”,不是“Looper 对象不能被其他线程持有”。

所以普通线程默认没有 Looper:

new Thread(() -> {
    Looper.myLooper(); // 默认是 null
}).start();

如果你想让普通线程变成 Handler 目标线程,必须让它准备 Looper 并进入 loop:

new Thread(() -> {
    Looper.prepare();
​
    Handler handler = new Handler(Looper.myLooper());
​
    Looper.loop();
}).start();

生产代码一般不会手写这段,而是用 HandlerThread 创建带 Looper 的工作线程。这里先记住这一句就够了:HandlerThread = Thread + Looper.prepare() + Looper.loop()。第三篇会专门展开它和主线程、Binder 线程的区别。

四、主线程为什么不会 main() 跑完就退出?

应用主线程入口在 ActivityThread.main()

如果把无关细节拿掉,主线是:

ActivityThread.main()
  -> Looper.prepareMainLooper()
  -> new ActivityThread()
  -> thread.attach(false, startSeq)
  -> Looper.loop()

源码入口:

这段源码只回答一个问题:

主线程为什么不会执行完 main() 就结束?

答案是:它最后进入了 Looper.loop()

主线程后续处理 Activity 生命周期、Service 回调、广播、输入、绘制调度,本质上都要通过这条长期运行的消息循环接住任务。主线程不是因为 Handler 而存在;而是主线程先有了 MainLooperHandler(Looper.getMainLooper()) 才能把任务投到它那里。

可以这样理解:

main() 不是主线程的终点。
Looper.loop() 才是主线程长期工作的入口。

五、post(Runnable) 到底做了什么?

post(Runnable) 的名字容易让人误会,以为它会直接执行 Runnable。源码里它只做一件事:把 Runnable 包装成 Message.callback,然后走消息发送流程。

逻辑骨架:

public final boolean post(Runnable r) {
    return sendMessageDelayed(getPostMessage(r), 0);
}
​
private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

源码入口:

所以 Runnable 并没有被马上调用。它只是被塞进了 Message.callback

接着 sendMessageDelayed() 会把 delay 转成一个绝对时间 when

public final boolean sendMessageDelayed(Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

post(r) 的 delay 是 0,所以 when 基本就是当前 uptime。后面第二篇会展开 when 为什么重要,这里只要先知道:消息入队时已经带着“什么时候可以被取出”的时间信息。

六、Message.target 是什么时候设置的?

这是 Handler 投递链路里最关键的一步。

sendMessageAtTime() 最后会走到 enqueueMessage()。在入队前,Handler 会把自己写进 msg.target

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    return queue.enqueueMessage(msg, uptimeMillis);
}

源码入口:

msg.target 的意义是:这条消息被目标线程取出来以后,要交回哪个 Handler 分发。

也就是说:

入队前:
    Handler 把自己写进 Message.target。
​
出队后:
    Looper 通过 msg.target 找回这个 Handler。

这就是为什么 Message 可以在队列里只作为数据存在,但被取出来以后还能回到正确的 Handler。

image.png

这张图要重点看两次交接:

第一次交接:
    Handler.enqueueMessage 把 msg.target 设置成当前 Handler。
​
第二次交接:
    Looper.loopOnce 取出 Message 后调用 msg.target.dispatchMessage(msg)。

dispatchMessage(msg)的三种回调分别是什么?

这里有两个名字很像、但完全不同的 callback:

  • msg.callback 是 Message 持有的 Runnable,由 post(Runnable) 写入;
  • mCallback 是 Handler 构造时传入的 Handler.Callback
  • handleMessage() 是 Handler 子类可以重写的兜底入口。
路径来源使用场景
msg.callbackpost(Runnable)在目标线程执行一段任务
mCallbackHandler 构造参数不继承 Handler,通过组合处理 Message
handleMessage()重写 Handler 方法通过 what 等字段集中处理普通 Message

三者不是并行调用,而是按顺序判断:

  1. 有 Runnable,直接执行并结束;
  2. 否则让 Handler.Callback 尝试消费;
  3. Callback 不存在或返回 false,最后进入 handleMessage。

七、Looper.loop 怎么把 Message 取出来?

目标线程长期跑在 Looper.loop() 里。

源码逻辑骨架:

public static void loop() {
    Looper me = myLooper();
    for (;;) {
        if (!loopOnce(me, ident, thresholdOverride)) {
            return;
        }
    }
}

loopOnce() 里会从 MessageQueue.next() 取下一条消息:

private static boolean loopOnce(...) {
    Message msg = me.mQueue.next();
    if (msg == null) {
        return false;
    }
​
    msg.target.dispatchMessage(msg);
    msg.recycleUnchecked();
    return true;
}

源码入口:

这里要抓住一个动作:

目标线程不是被 post 直接推着跑。
目标线程自己在 loop 中主动从 MessageQueue.next() 取消息。

所以当你问“为什么 Runnable 在主线程执行”时,答案不是“因为子线程把它切过去了”,而是:

因为 mainHandler 持有 MainLooper 引用;
Runnable 被包装成 Message 放入 MainLooper 的 MessageQueue;
主线程正在 Looper.loop();
主线程从自己的 MessageQueue 取出这条 Message;
最后由主线程调用 Runnable.run()。

八、dispatchMessage 为什么先走 Runnable,而不是 handleMessage?

很多人遇到过这种问题:

Handler handler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
        // 为什么 handler.post(runnable) 不进这里?
    }
};
​
handler.post(() -> doWork());

原因在 dispatchMessage()

逻辑骨架:

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

源码入口:

优先级是:

1. 如果 msg.callback != null:
       执行 Runnable.run()
​
2. 否则如果 Handler 构造时传了 Callback:
       执行 Callback.handleMessage()
​
3. 否则:
       执行 Handler.handleMessage()

post(Runnable) 会把 Runnable 放进 Message.callback,所以它优先走第一段。它一般不会走你重写的 handleMessage()

这也是为什么排查“Handler 没进 handleMessage”时,要先看发送方式:

sendMessage(msg):
    通常走 Callback 或 handleMessage。
​
post(runnable):
    通常走 Message.callback,也就是 Runnable.run

九、所以 Runnable 最后到底在哪个线程执行?

回到开头那个例子:

Handler mainHandler = new Handler(Looper.getMainLooper());
​
new Thread(() -> {
    mainHandler.post(() -> updateUi());
}).start();

按源码链路拆开:

子线程
  -> mainHandler.post(runnable)
  -> getPostMessage(runnable)
  -> msg.callback = runnable
  -> sendMessageAtTime(msg, now)
  -> msg.target = mainHandler
  -> MainLooper.mQueue.enqueueMessage(msg)
​
主线程
  -> Looper.loop()
  -> MainLooper.mQueue.next()
  -> 取出 msg
  -> msg.target.dispatchMessage(msg)
  -> msg.callback.run()
  -> updateUi()

所以最终执行线程是:

mainHandler 持有的 Looper 所在线程。

不是:

调用 post 的线程。

也不是:

Handler 对象被 new 出来的线程。

如果一个 Handler 是这样创建的:

Handler workerHandler = new Handler(workerThread.getLooper());

那么无论你在主线程、Binder 线程、线程池线程里调用 workerHandler.post(),最后执行 Runnable.run() 的都是 workerThread.getLooper() 所在线程。

十、模糊说法 vs 准确说法

第一组:

模糊说法:
    Handler 切线程。
​
准确说法:
    Handler 把任务投递到目标 Looper 的 MessageQueue,
    真正执行发生在目标 Looper 所在线程。

第二组:

模糊说法:
    Handler 是主线程。
​
准确说法:
    Handler 持有某个 Looper 引用;
    如果它持有 MainLooper,消息才会在主线程执行。

第三组:

模糊说法:
    post 会马上执行。
​
准确说法:
    post 只是入队;
    什么时候执行取决于目标 MessageQueue 和 Looper。

第四组:

模糊说法:
    handler.post 没进 handleMessage,说明 Handler 没执行。
​
准确说法:
    post(Runnable) 会优先走 Message.callback,
    所以执行的是 Runnable.run,不是 handleMessage。

十一、结论

handler.post() 不是魔法切线程。

它只是把 Runnable 包装成 Message,放进 Handler 所持 Looper 的 MessageQueue

真正执行 Runnable 的,不是调用 post() 的线程,而是目标 Looper 所在线程。

一句话总结:

调用线程只负责入队,目标 Looper 线程负责取出并执行。

这篇解决的是“任务怎么从调用线程进入目标线程”。但任务入队以后,还有第二个问题:

Message 已经进队列了,为什么不是马上执行?

下一篇就讲这个:MessageQueue 为什么不是普通队列?

延伸阅读