咱们今天要把 Android 的 Handler 消息队列变成一个你绝对能听懂的有趣故事。我是你们村的快递员老 H(Handler),专门负责给大家派送"任务包裹"(Message)。
故事背景:幸福村的快递站
在我们幸福村,有一个核心的快递总站(MessageQueue) ,这个站里有一条长长的传送带(链表结构的消息队列) ,包裹按照"最急件"到"普通件"的顺序排列。传送带旁边,睡着一位超级敬业的快递分拣员小 L(Looper) ,他的工作非常简单但至关重要:永远不停地巡视传送带,看看最前面的包裹是不是到派送时间了。
我呢,就是快递员老 H(Handler),我的职责是接收村民(例如主线程)的任务,把它们打包成包裹,然后投递到快递总站的传送带上。
第一幕:第一个任务来了!
村民说: "老 H,10秒后帮我把这封信送了(postDelay 10s)。"
老 H(Handler)的行动:
- 打包: 我拿出一个空包裹(
Message
),把信(Runnable任务)塞进去。 - 贴标签: 我在包裹上贴上一个非常重要的标签: "派送时间 = 当前时间(SystemClock.uptimeMillis()) + 10000毫秒" 。这个时间点我们称之为
when
。 - 送往快递站: 我拿着这个包裹跑到快递总站(
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)的同样流程:
- 打包贴标签: 标签上写着 "派送时间 = 当前时间 + 1000毫秒" 。注意,此时的"当前时间"已经比第一个任务的时间点晚了1秒。
- 送往快递站: 我又拿着这个新包裹(包裹2)跑到快递站。
高潮部分:快递总站(MessageQueue)的智能排序!
我(MessageQueue.enqueueMessage
方法)拿着新包裹,看着传送带,心里开始盘算:
"第一个包裹的派送时间是 T+10秒,我这个新包裹的派送时间是 T+1秒,新包裹的派送时间
when
是当前时刻+1s
,它肯定会比那个 T+10s 的包裹要早。"
所以,我不能简单地把它放在最后面,我必须把它插队到第一个包裹的前面! 因为传送带的原则是:派送时间最早的包裹,永远放在最前面(一个按 when
从小到大排序的优先级队列)。
插入过程如下:
- 我比较新包裹(when = T+1s)和当前第一个包裹(when = T+10s)。
- 因为 T+1s < T+10s,新包裹更紧急。
- 于是,我把新包裹放在传送带的最开头,然后把原来的第一个包裹接在新包裹后面。
代码视角(简化版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()
方法就是他的核心工作:
- 他看向传送带最前面的包裹2。
- 计算一下需要等待的时间:
等待时间 = 包裹2的when - 当前时间
。 - 如果等待时间 > 0,他就再睡这么一小会儿。现在他只需要睡大约1秒。
- 1秒后,小 L 准时醒来,取下包裹2,走出快递站,把它交给包裹上指定的快递员老 H(
msg.target.dispatchMessage(msg)
)。 - 老 H 拆开包裹,执行里面的任务(Runnable)。
- 完成后,小 L 回到传送带旁,发现第一个包裹变成了包裹1。他计算了一下,距离派送时间还有大概9秒,于是他又安心地睡9秒的觉去了。
- 9秒后,醒来,派送包裹1。
总结原理
- 核心结构: MessageQueue 是一个按消息的派送时间(
when
)从小到大排序的优先级队列,实现上是单链表。 - 入队(enqueueMessage): 当新消息加入时,会根据其
when
值找到合适的位置插入 ,保证队首的消息总是最早要执行的。如果新消息比原来的队首消息更早,就会唤醒Looper线程。 - 出队(next): Looper 在循环中调用
next()
取消息。如果队首消息的when
还没到,就计算时间差并进行精确阻塞(nativePollOnce) 。阻塞可能被新消息的插入(唤醒)提前打断。 - 处理你的场景:
postDelay(10s)
的消息先入队,排在队首。当postDelay(1s)
的消息入队时,因为它的实际派送时间更早,所以它会 "插队"到10s消息的前面 ,并且立即唤醒Looper。Looper被唤醒后,会等待1秒(而不是原来的10秒),然后先处理1s的消息,再继续等待9秒处理10s的消息。
这样一来,即使任务发送的顺序有先后,但执行顺序总是由延迟时间决定的。这就是 Handler 延迟消息的智能之处!怎么样,这个故事是不是让你彻底明白了?