Handler 运行流程源码解析:从 sendMessage 到 handleMessage 的完整链路

文章内容

本文内容整理自技术讲解视频《handler 运行的整个流程源码解析》。

主线程里有一个 for (;;) 死循环,从应用启动一直转到进程结束,应用却不卡。死循环与不卡顿为什么能同时成立?

原因可以总结为:send 系列方法把消息交给 Handler.enqueueMessage,消息带着一个执行时刻 when 被插进 MessageQueue 的单向链表(按 when 升序,所以它是优先级队列而不是 FIFO 队列),Looper.loop() 的死循环反复调 MessageQueue.next() 取队头,取到后调 msg.target.dispatchMessage(msg),最终落到 handleMessage。入队与出队的线程安全靠 synchronized (this);死循环不烧 CPU,靠 nativePollOnce 在 native 层的 epoll 上阻塞休眠。

hero

落到代码上,子线程发消息、主线程处理的最小写法是这样:

java
// 主线程:Looper 已由 ActivityThread.main() 准备好,直接绑定即可Handler handler = new Handler(Looper.getMainLooper()) {    @Override    public void handleMessage(Message msg) {        // 这里已经回到主线程        textView.setText("got: " + msg.what);    }}; // 任意子线程:handler.sendEmptyMessage(1);              // 立即入队,when = nowhandler.sendEmptyMessageDelayed(2, 5000); // when = now + 5000handler.post(() -> textView.setText("ok")); // 包成 msg.callback 后走同一条链

从 sendEmptyMessage(1) 这一行到 handleMessage 被调用,中间的完整调用链是:

send_dispatch_sequence

本文所有源码引用基于 AOSP 的 frameworks/base/core/java/android/os/ 下的 Handler.java、MessageQueue.java、Looper.java、Message.java。

一、全局概览:四个组件各管什么

1.1 简介

把 Handler 想成传送带:一端放东西,另一端有人取走处理。这个类比能解释「发送方和处理方在不同线程」这件事,但有两处会误导。

传送带按放上去的先后顺序送达,MessageQueue 按 when 排序,后发的消息完全可能排在先发的消息前面(比如 postDelayed(r, 5000) 之后紧接着 post(r2),r2 会插到 r 前面)。传送带空转时仍在耗电,Looper 在队列为空时会睡死在 epoll 上,CPU 占用是零。后面这条正是死循环不卡应用的答案。

1.2 组件边界

Thread 是宿主,一个线程最多绑一个 Looper。

Looper 通过 prepare() 配合 ThreadLocal<Looper> sThreadLocal 保证线程内唯一;loop() 是那个死循环,负责驱动整个消息泵。

MessageQueue 是消息的存放处,内部结构是单向链表 Message mMessages(队头),节点按 when 升序排列。对外只有两个动作:enqueueMessage(Message, long) 入队、next() 出队。

Handler 是使用者的门面,持有 mLooper 和 mQueue 两个引用。它的十几个 send/post 方法最终全部收敛到同一个私有出口,handleMessage 是留给使用者重写的回调。

Message 是载体,三个字段把它的去向定下来:target 指向发它的 Handler,决定消息最后回谁处理;when 是基于 SystemClock.uptimeMillis() 的执行时刻,决定它在队列里排第几;callback 是 post(Runnable) 传进来的那个 Runnable。

flow

二、发送端:十几个入口,一个出口

2.1 收敛路径

sendMessage(msg) 只做一件事:调 sendMessageDelayed(msg, 0)。后者把延迟换算成绝对时刻,调 sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)。sendMessageAtTime 取出 mQueue,调 enqueueMessage(queue, msg, uptimeMillis)。

post(Runnable r) 的路径几乎一样,区别只在第一步:getPostMessage(r) 先把 Runnable 包成一个 msg.callback = r 的 Message,之后走同一条链。sendEmptyMessage(int what) 则是先 Message.obtain() 再 setWhat。

所以 Handler 对外暴露的 send* 与 post*(含 postAtTime、postAtFrontOfQueue、sendEmptyMessageAtTime、sendMessageAtFrontOfQueue 等重载)总数在十三到十四个之间,随 API 版本略有出入(postDelayed(Runnable, Object, long) 是 API 28 才加的)。它们无一例外,终点都是 Handler.enqueueMessage。在 AOSP 目录下跑这个命令可以自己数一遍:

bash
grep -nE "public .*(send|post)[A-Za-z]*\(" frameworks/base/core/java/android/os/Handler.java

只有 sendMessageAtFrontOfQueue 稍有不同:它直接传 when = 0,让消息插到队头,绕过时间排序。这个方法在框架内部有少量使用,业务代码里几乎也不用。

send_chain

2.2 Handler.enqueueMessage 只做一件事

msg.target 是在这个方法里被赋值的,消息和 Handler 的绑定关系在此建立,而非在构造 Message 时。

java
// Handler.java —— 所有 send/post 的共同出口private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {    // target 在这里绑定:出队后 Looper 靠它找回处理者    msg.target = this;    if (mAsynchronous) {        msg.setAsynchronous(true);    }    return queue.enqueueMessage(msg, uptimeMillis);}

输入是队列、消息和绝对时刻,输出是入队是否成功,状态变化只有一处:msg.target 被写成当前 Handler。有两个推论:消息回到哪个 Handler 在发送时就已确定,与出队时的上下文无关;一个线程可以有任意多个 Handler,它们共用同一条 mQueue,靠各自消息里的 target 区分归属。

三、MessageQueue 为什么是优先级队列

3.1 插入点怎么找

链表本身不会自动排序,顺序是插入时维护出来的。MessageQueue.enqueueMessage 的主体就是一次带比较的链表插入。

java
// MessageQueue.java —— 入队即排序,队列的"优先级"是在这里建立的boolean enqueueMessage(Message msg, long when) {    // 没有 target 的消息是同步屏障,业务代码走到这里说明用错了    if (msg.target == null) {        throw new IllegalStateException("Message must have a target.");    }    // 全程持锁:多个 Handler 可能来自不同线程    synchronized (this) {        // 消息泵已退出,直接回收,不再接收新消息        if (mQuitting) {            msg.recycle();            return false;        }        msg.when = when;        Message p = mMessages;        boolean needWake;        // 队列为空,或新消息比队头还早:插到头部        if (p == null || when == 0 || when < p.when) {            msg.next = p;            mMessages = msg;            needWake = mBlocked;        } else {            needWake = false;            Message prev;            // 向后遍历,遇到第一个 when 比自己晚的节点就停,插在它前面            for (;;) {                prev = p;                p = p.next;                if (p == null || when < p.when) {                    break;                }            }            msg.next = p;            prev.next = msg;        }        // 插到队头时,唤醒可能正睡在 epoll 上的 Looper 重算超时        if (needWake) {            nativeWake(mPtr);        }    }    return true;}

这个方法把 msg 按 when 插进链表:插到队头会改写 mMessages,插到中间改写 prev.next,成功返回 true;仅在消息泵已退出时才回收消息、返回 false。若插到队头且 mBlocked 为真,还会 nativeWake 叫醒 epoll 上睡着的 Looper。

这段插入能推出几点。队列的有序性来自插入时的线性查找,最坏复杂度 O(n)(队列里堆上千条延迟消息时,每次入队都要走一遍链表)。其次,when 是唯一的排序依据,跟调用 sendMessage 的先后无关。另外 p.when 与新消息 when 相等时循环继续往后走(when < p.when 才 break),所以同一时刻的消息按入队顺序排,相同 when 的消息之间是稳定的 FIFO。

queue_sort

3.2 锁保护的是什么

synchronized (this) 锁的是 MessageQueue 实例本身,next() 里用的是同一把锁。这是必要的:一个 UI 线程的 MessageQueue,可能同时被主线程的 Handler、若干工作线程的 Handler、以及 Looper 自己的 next() 访问。链表插入是多步操作(改 msg.next、改 prev.next 或 mMessages),中途被打断会直接断链。

所以「多个 Handler 跨线程往 MessageQueue 添加数据如何保证线程安全」这个问题的答案有两层:enqueueMessage 与 next() 共用一把对象锁做互斥,加上 nativeWake/nativePollOnce 这对唤醒—休眠原语协调阻塞状态。缺了后者,锁能保证正确性,但新插入的队头消息要等到旧的超时到期才被发现。

3.3 postDelayed 之后队列发生了什么

postDelayed(r, 3000) 展开后是:getPostMessage(r) 造出一条 callback = r 的消息 → when = uptimeMillis() + 3000 → 走上面那段插入逻辑。

队列的变化取决于这条消息落在哪里。如果它比现有队头还早(比如队列里全是 10 秒后才执行的消息),它成为新队头,mBlocked 为真则触发 nativeWake,正在 epoll 上等待的 Looper 被叫醒,重新计算下一次超时为 3000ms。如果它排在中间,链表插入完成,Looper 完全不需要被打扰(它现有的超时时间仍然指向更早的那条消息)。

顺带澄清一个常见误解:延迟消息不会占用任何定时器线程,也没有轮询。整个延迟机制就是「排序 + 一次 epoll 超时等待」。

四、Looper 死循环为什么不会卡死应用

4.1 next() 里的阻塞点

答案在 MessageQueue.next() 里,「死循环不耗 CPU」的阻塞触发点:

java
// MessageQueue.java —— 阻塞发生在这里,不在 Looper.loop()Message next() {    int nextPollTimeoutMillis = 0;    for (;;) {        // native 层 epoll_wait:0 立即返回,-1 无限期睡,正数睡到超时        nativePollOnce(mPtr, nextPollTimeoutMillis);        synchronized (this) {            final long now = SystemClock.uptimeMillis();            Message msg = mMessages;            if (msg != null) {                if (now < msg.when) {                    // 队头还没到执行时刻,算出精确的剩余毫秒数继续睡                    nextPollTimeoutMillis =                            (int) Math.min(msg.when - now, Integer.MAX_VALUE);                } else {                    // 到点,摘下队头返回给 Looper                    mBlocked = false;                    mMessages = msg.next;                    msg.next = null;                    msg.markInUse();                    return msg;                }            } else {                // 队列空,无限期休眠,等 nativeWake 或 fd 事件                nextPollTimeoutMillis = -1;            }            mBlocked = true;        }    }}

nativePollOnce 是 native 方法,实现在 frameworks/base/core/jni/android_os_MessageQueue.cpp,最终落到 Looper::pollOnce,用的是 Linux 的 epoll_wait。线程在 epoll_wait 上是真正的休眠状态,不参与 CPU 调度,不消耗时间片。

队列空时线程睡在 nativePollOnce(mPtr, -1) 上;延迟消息的精确触发靠 nextPollTimeoutMillis 算出 msg.when - now,把等待交给 epoll 的超时。至于为什么外面还要套一层 for (;;):nativePollOnce 可能因为 native 端的其他 fd 事件提前返回,此时队头仍未到期,需要重新计算超时再睡一次。

4.2 loop() 的主体

Looper.loop() 本身很薄,删掉日志和 Trace 之后只剩几行。

java
// Looper.java —— 死循环的全部内容public static void loop() {    final Looper me = myLooper();    if (me == null) {        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");    }    final MessageQueue queue = me.mQueue;    for (;;) {        // 队列空或未到点时阻塞在这里;返回 null 只发生在 quit() 之后        Message msg = queue.next();        if (msg == null) {            return;        }        // target 是发送时绑定的 Handler,消息由此回到正确的处理者        msg.target.dispatchMessage(msg);        // 清空所有字段后放回全局对象池        msg.recycleUnchecked();    }}

for (;;) 里没有任何 sleep、没有轮询间隔、没有 CPU 让渡逻辑,这些都不需要,阻塞已经在 queue.next() 内部完成了。循环的唯一出口是 next() 返回 null,而这只在 quit() 把 mQuitting 置真之后发生。主线程的 Looper 由 ActivityThread.main() 启动,正常情况下不退出。

loop_next

4.3 那 ANR 是怎么来的

死循环和 ANR 没有因果关系。ANR 的判定逻辑是:某类事件(输入事件 5 秒、前台 Service 20 秒、BroadcastReceiver 10 秒)在规定时间内没有被处理完。这些事件最终都是以消息的形式进入主线程 MessageQueue 的,超时的原因是某条消息的 handleMessage(或某个 Runnable)执行时间过长,把后面的消息全堵在队列里。

导致 ANR 的是循环体里某次 dispatchMessage 迟迟不返回,不是循环本身——循环停了应用直接退出,不会卡住。

界面刷新走的是另一条路:Choreographer 向 SurfaceFlinger 注册 VSYNC 信号,信号到来时通过 native 层的 fd 事件唤醒同一个 Looper,再以异步消息的形式执行 doFrame。这条路仍然复用了 Handler 机制,只是入口从 sendMessage 换成了 native fd 事件。

五、分发端:dispatchMessage 的三段优先级

消息出队后交给 msg.target.dispatchMessage(msg),这里决定了「重写 handleMessage」和「post 一个 Runnable」这两种写法为什么可以共存。

java
// Handler.java —— 分发的三级优先级,顺序固定public void dispatchMessage(Message msg) {    // 第一级:post(Runnable) 塞进来的 callback,直接 run,不经过 handleMessage    if (msg.callback != null) {        handleCallback(msg);    } else {        // 第二级:构造 Handler 时传入的 Callback,返回 true 表示消息被消费掉        if (mCallback != null) {            if (mCallback.handleMessage(msg)) {                return;            }        }        // 第三级:子类重写的 handleMessage        handleMessage(msg);    }}

把三级同时装到一个 Handler 上,最能看清它们的抢占关系:

java
Handler.Callback callback = msg -> {    Log.d(TAG, "第二级 Callback: what=" + msg.what);    return msg.what == 1;   // what==1 拦截;其余放行}; Handler h = new Handler(Looper.getMainLooper(), callback) {    @Override    public void handleMessage(Message msg) {        Log.d(TAG, "第三级 handleMessage: what=" + msg.what);    }}; h.sendEmptyMessage(1);              // Ah.sendEmptyMessage(2);              // Bh.post(() -> Log.d(TAG, "第一级 callback.run()"));  // C

输出:

plain text
第二级 Callback: what=1                 ← A:Callback 返回 true,就此终止第二级 Callback: what=2                 ← B:Callback 返回 false…第三级 handleMessage: what=2            ←   …继续落到子类 handleMessage第一级 callback.run()                   ← C:msg.callback != null,两级 Callback 全部跳过

三条消息各自停在不同的级别:A 被第二级吃掉,B 穿过第二级落到第三级,C 连第二级都没进(post 的 Runnable 存在 msg.callback 里,dispatchMessage 第一个 if 就分流走了)。所以同一个 Handler 上既传了 Callback、又重写了 handleMessage、还用 post 发任务时,这三套回调彼此不会互相触发。

所以三级之间是互斥的,先后顺序也固定。post 进来的消息永远不会触发 handleMessage;new Handler(callback) 传入的 Callback 返回 true 会拦截消息,返回 false 则继续往下走到子类的 handleMessage。这个 Callback 拦截机制是很多 Hook 方案(比如替换 ActivityThread.mH 的 Callback 来干预启动流程)的落地点。

六、一个线程一个 Looper:ThreadLocal 的强制约束

Looper.prepare() 只有五行,「一线程一 Looper」就靠这五行保证。

java
// Looper.java —— sThreadLocal 是 static 的,每个线程各存一份自己的 Looperprivate 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));}

sThreadLocal 是 static final ThreadLocal<Looper>,值存在每个 Thread 对象自己的 ThreadLocalMap 里,线程之间天然隔离。new Looper(quitAllowed) 的构造函数里会 mQueue = new MessageQueue(quitAllowed),Looper 和 MessageQueue 是一比一创建的。

主线程不需要手动 prepare,因为 ActivityThread.main() 里已经调了 Looper.prepareMainLooper()(内部是 prepare(false),主线程的 Looper 不允许 quit),随后 Looper.loop()。这也解释了为什么主线程可以直接 new Handler() 而子线程不行(Handler 的构造函数里有 mLooper = Looper.myLooper(),取到 null 就抛 "Can't create handler inside thread that has not called Looper.prepare()")。子线程要用 Handler,必须自己走一遍 Looper.prepare() → new Handler() → Looper.loop()。

所以数量关系是:一个线程一个 Looper、一条 MessageQueue,但可以有任意多个 Handler。Handler 只是队列的操作接口,它的构造函数不做任何计数或去重。

七、实践结论

**Handler 内存泄漏的成因:

  • **非静态内部类 Handler 隐式持有外部 Activity 的引用。发出去的消息带着 target 指向这个 Handler,只要消息还在 MessageQueue 里(延迟消息尤其明显),引用链就是 MessageQueue → Message → Handler → Activity。而 MessageQueue 由主线程 Looper 持有,生命周期和进程一样长。Activity 已经 finish 但内存回收不掉,根因就在这条链上。两种断链方式:把 Handler 写成静态内部类并用 WeakReference 持有 Activity,或者在 onDestroy 里调 removeCallbacksAndMessages(null) 把队列里属于这个 Handler 的消息全部清掉。后者更彻底,因为它同时解决了「Activity 销毁后延迟消息仍然执行」的空指针问题。

**Message 的创建方式:

  • ** 用 Message.obtain() 或 handler.obtainMessage(),不要 new Message()。Message 内部维护一个静态链表对象池(sPool,上限 MAX_POOL_SIZE = 50),obtain() 从池头摘一个复用,池空时才 new。回收由 Looper.loop() 里的 msg.recycleUnchecked() 自动完成,它会清空所有字段再放回池里。高频消息场景下这一条能明显减少小对象分配和 GC 压力。handler.obtainMessage() 比 Message.obtain() 更省一步,它顺手把 target 设成了当前 Handler。

**子线程 Looper 的用途:

  • **一个自带 Looper 的子线程就是一条串行任务队列:外部通过 Handler 往里投任务,任务在该线程上按时间顺序逐个执行,空闲时线程休眠不占 CPU。HandlerThread 就是对这套模板的封装,IntentService 内部用的正是它。相比每次起新线程,这种模式省掉了线程创建开销,也天然规避了任务之间的并发问题。

八、尝试

查看队列的实时状态,直接 dump:

java
// 打印当前 Looper 的 MessageQueue 里所有待处理消息,含 when 和 targetLooper.getMainLooper().dump(new LogPrinter(Log.DEBUG, "MQ"), "");

输出里能看到每条消息的 what、when(相对当前时间的毫秒数)和 target 类名。连着 postDelayed 几条不同延迟的消息再 dump,可以直接观察到它们按 when 排序而非按发送顺序排列。

验证 Looper 休眠不占 CPU:应用静置时用 adb shell top -H -p <pid> 看主线程,CPU 占用应当是 0。如果不是,说明有消息在被反复投递(常见于某个 postDelayed 自己调自己的定时任务)。

排查主线程卡顿,入口是 Looper.setMessageLogging(Printer)。Looper 在 dispatchMessage 前后各打一次日志(">>>>> Dispatching to " 和 "<<<<< Finished to "),拦下这个 Printer 计算两次回调的时间差,就能定位到具体是哪个 Handler 的哪条消息执行超时。BlockCanary 这类工具的原理就是这个。

回到最初那个问题:一条消息从 sendMessage 到 handleMessage,中间经过绑定 target、按 when 排序入队、Looper 从 epoll 中被唤醒、next() 摘取队头、dispatchMessage 三级分发。MessageQueue 按 when 排序,所以是优先级队列而非 FIFO;阻塞发生在 epoll_wait 上,死循环不烧 CPU。


本文由 AgentPlanFlow 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Handler 运行流程源码解析:从 sendMessage 到 handleMessage 的完整链路 · Yinchen 的博客