浅析Hanlder处理延时消息的流程

咱们今天要把 Android 的 Handler 消息队列变成一个你绝对能听懂的有趣故事。我是你们村的快递员老 H(Handler),专门负责给大家派送"任务包裹"(Message)。

故事背景:幸福村的快递站

在我们幸福村,有一个核心的快递总站(MessageQueue) ,这个站里有一条长长的传送带(链表结构的消息队列) ,包裹按照"最急件"到"普通件"的顺序排列。传送带旁边,睡着一位超级敬业的快递分拣员小 L(Looper) ,他的工作非常简单但至关重要:永远不停地巡视传送带,看看最前面的包裹是不是到派送时间了。

我呢,就是快递员老 H(Handler),我的职责是接收村民(例如主线程)的任务,把它们打包成包裹,然后投递到快递总站的传送带上


第一幕:第一个任务来了!

村民说: "老 H,10秒后帮我把这封信送了(postDelay 10s)。"

老 H(Handler)的行动:

  1. 打包: 我拿出一个空包裹(Message),把信(Runnable任务)塞进去。
  2. 贴标签: 我在包裹上贴上一个非常重要的标签: "派送时间 = 当前时间(SystemClock.uptimeMillis()) + 10000毫秒" 。这个时间点我们称之为 when
  3. 送往快递站: 我拿着这个包裹跑到快递总站(MessageQueue)。

快递总站(MessageQueue)的内部操作:

传送带现在是空的。小 L(Looper)正在打盹,因为没活儿干。

我(作为Handler,实际是MessageQueue.enqueueMessage方法)把包裹放上传送带。因为它是唯一的一个,所以就放在最开头。

代码视角:

java 复制代码
// Handler.java
public final boolean postDelayed(Runnable r, long delayMillis) {
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}

public final boolean sendMessageDelayed(Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    // 关键计算:派送时间 = 当前时间 + 延迟时间
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue; // 找到快递站
    if (queue == null) return false;
    // 把包裹和派送时间交给快递站!
    return enqueueMessage(queue, msg, uptimeMillis);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this; // 标记这个包裹是我老H的
    return queue.enqueueMessage(msg, uptimeMillis); // 真正入队的地方
}

此时,传送带的状态是:

text 复制代码
[ 包裹1 (when = 现在+10s) ] -> NULL

小 L(Looper)醒来,看了一眼第一个包裹的派送时间,发现还早着呢!于是他决定躺下睡到那个时间点再醒(这就是线程阻塞)。


第二幕:第二个紧急任务!

接着,另一个村民急匆匆跑来: "老 H,这个件很急,1秒后就要送!(postDelay 1s)"

老 H(Handler)的同样流程:

  1. 打包贴标签: 标签上写着 "派送时间 = 当前时间 + 1000毫秒" 。注意,此时的"当前时间"已经比第一个任务的时间点晚了1秒。
  2. 送往快递站: 我又拿着这个新包裹(包裹2)跑到快递站。

高潮部分:快递总站(MessageQueue)的智能排序!

我(MessageQueue.enqueueMessage方法)拿着新包裹,看着传送带,心里开始盘算:

"第一个包裹的派送时间是 T+10秒,我这个新包裹的派送时间是 T+1秒,新包裹的派送时间 when当前时刻+1s,它肯定会比那个 T+10s 的包裹要早。"

所以,我不能简单地把它放在最后面,我必须把它插队到第一个包裹的前面! 因为传送带的原则是:派送时间最早的包裹,永远放在最前面(一个按 when 从小到大排序的优先级队列)。

插入过程如下:

  1. 我比较新包裹(when = T+1s)和当前第一个包裹(when = T+10s)。
  2. 因为 T+1s < T+10s,新包裹更紧急。
  3. 于是,我把新包裹放在传送带的最开头,然后把原来的第一个包裹接在新包裹后面。

代码视角(简化版MessageQueue.enqueueMessage):

java 复制代码
boolean enqueueMessage(Message msg, long when) {
    synchronized (this) {
        // ... 省略一些处理 ...
        msg.when = when;
        Message p = mMessages; // p 指向当前传送带上的第一个包裹
        if (p == null || when == 0 || when < p.when) {
            // 情况1:传送带是空的,或者新包裹比第一个包裹还急
            // 插队!成为新的第一名!
            msg.next = p;
            mMessages = msg;
            // 重要!如果分拣员在睡觉,需要唤醒他!(因为来了个更急的件)
            needWake = mBlocked;
        } else {
            // 情况2:需要插在队伍中间
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    // 找到了插入位置:在prev和p之间
                    break;
                }
            }
            msg.next = p;
            prev.next = msg;
        }
        // 如果需要,就唤醒小L
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

插入完成后,传送带的状态变成了:

text 复制代码
[ 包裹2 (when = 现在+1s) ] -> [ 包裹1 (when = 现在+10s) ] -> NULL

关键一步:唤醒小 L!

因为小 L 之前正在睡长觉(等待10秒),但现在来了一个更急的包裹(只需要再等1秒),我必须立刻把他叫醒(nativeWake)!告诉他:"别睡啦!有急件!"

小 L 被叫醒,有点懵,但还是敬业地重新开始巡视。


第三幕:派送开始!

小 L(Looper)的工作循环(loop()方法)是这样的:

java 复制代码
public static void loop() {
    // ...
    for (;;) {
        // 去快递站取下一个要派送的包裹(可能会阻塞)
        Message msg = queue.next();
        if (msg == null) {
            return;
        }
        // 派送包裹:把包裹交给快递员老H(msg.target)
        msg.target.dispatchMessage(msg);
        // ...
    }
}

queue.next() 方法就是他的核心工作:

  1. 他看向传送带最前面的包裹2。
  2. 计算一下需要等待的时间:等待时间 = 包裹2的when - 当前时间
  3. 如果等待时间 > 0,他就再睡这么一小会儿。现在他只需要睡大约1秒。
  4. 1秒后,小 L 准时醒来,取下包裹2,走出快递站,把它交给包裹上指定的快递员老 H(msg.target.dispatchMessage(msg))。
  5. 老 H 拆开包裹,执行里面的任务(Runnable)。
  6. 完成后,小 L 回到传送带旁,发现第一个包裹变成了包裹1。他计算了一下,距离派送时间还有大概9秒,于是他又安心地睡9秒的觉去了。
  7. 9秒后,醒来,派送包裹1。

总结原理

  1. 核心结构: MessageQueue 是一个按消息的派送时间(when)从小到大排序的优先级队列,实现上是单链表。
  2. 入队(enqueueMessage): 当新消息加入时,会根据其 when找到合适的位置插入 ,保证队首的消息总是最早要执行的。如果新消息比原来的队首消息更早,就会唤醒Looper线程。
  3. 出队(next): Looper 在循环中调用 next() 取消息。如果队首消息的 when 还没到,就计算时间差并进行精确阻塞(nativePollOnce) 。阻塞可能被新消息的插入(唤醒)提前打断。
  4. 处理你的场景: postDelay(10s) 的消息先入队,排在队首。当 postDelay(1s) 的消息入队时,因为它的实际派送时间更早,所以它会 "插队"到10s消息的前面 ,并且立即唤醒Looper。Looper被唤醒后,会等待1秒(而不是原来的10秒),然后先处理1s的消息,再继续等待9秒处理10s的消息。

这样一来,即使任务发送的顺序有先后,但执行顺序总是由延迟时间决定的。这就是 Handler 延迟消息的智能之处!怎么样,这个故事是不是让你彻底明白了?

相关推荐
用户092 小时前
Android面试基础篇(一):基础架构与核心组件深度剖析
android·面试·kotlin
wow_DG4 小时前
【MySQL✨】MySQL 入门之旅 · 第十篇:数据库备份与恢复
android·数据库·mysql
00后程序员张4 小时前
iOS 26 系统流畅度深度剖析,Liquid Glass 视效与界面滑动的实际测评
android·macos·ios·小程序·uni-app·cocoa·iphone
草字5 小时前
Android studio 查看apk的包名,查看包名
android·ide·android studio
、BeYourself5 小时前
Android Studio 详细安装与配置指南
android
夜晚中的人海5 小时前
C++11(2)
android·数据库·c++
Kapaseker7 小时前
每个Kotlin开发者应该掌握的最佳实践,最后一趴
android·kotlin
每次的天空7 小时前
Android -自定义Binding Adapter实战应用
android
每次的天空8 小时前
Android-Git技术总结
android·学习