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 的稳定机制,不展开版本实现差异。
整条最小链路是:
调用线程
-> 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 而存在;而是主线程先有了 MainLooper,Handler(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。
这张图要重点看两次交接:
第一次交接:
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.callback | post(Runnable) | 在目标线程执行一段任务 |
mCallback | Handler 构造参数 | 不继承 Handler,通过组合处理 Message |
handleMessage() | 重写 Handler 方法 | 通过 what 等字段集中处理普通 Message |
三者不是并行调用,而是按顺序判断:
- 有 Runnable,直接执行并结束;
- 否则让 Handler.Callback 尝试消费;
- 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 为什么不是普通队列?