消息机制的结构约束
“这段代码用了什么设计模式”不是阅读 Android 消息机制的好入口。类图相似并不代表运行时 契约相同:Message 没有统一的 execute() 接口,MessageQueue 也不是普通 FIFO 队列, Handler.dispatchMessageImpl() 更不是可以任意串接的通用责任链。
本文面向已经读过 Handler Executor 提交边界、 Handler 消息处理、消息队列性能代价 的读者。问题边界限定在 Java 消息机制如何封装请求、交给队列、由一个 Looper 消费,再选择 回调并回收对象;不把这些结构推广成所有 Android 异步框架的共同实现。
读完后,读者应能从源码回答四个问题:请求的状态存在哪里,谁拥有队列,何时发生消费, 取消或退出时哪些引用会被清理。文中“模式”只是帮助命名不变量的解释工具;最终结论以 字段写入、锁保护、返回值和调用顺序为准。
1. 识别方法
入口先行
先选一个可执行入口,再沿着字段和调用关系追踪,而不是从 Handler 的类名猜模式。以 post(Runnable) 为例,入口是 Handler.post(),直接调用 sendMessageDelayed(),而不是 启动线程:
源码文件:frameworks/base/core/java/android/os/Handler.java
public final boolean post(@NonNull Runnable r) {
return sendMessageDelayed(getPostMessage(r), 0);
}
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}这两行已经给出第一条不变量:Runnable 的身份先被写进 Message,之后的队列操作处理的是 Message,而不是直接处理 Runnable。若要判断这是“命令对象”还是单纯的请求载体,还必须继续 查找 target、when、分发入口和回收路径。
不变量成对
阅读每个候选结构时,至少同时回答以下配对问题:
| 观察点 | 要找的源码证据 |
|---|---|
| 请求封装 | 请求数据写入哪些字段,谁读取它们 |
| 所有权 | 队列和状态由哪个对象持有,哪个锁保护 |
| 生效时机 | 入队、到期、出队、回调和回收分别何时发生 |
| 失败路径 | 入队失败、执行抛异常、退出和取消如何传播 |
| 清理路径 | next、target、callback、监听器和计数何时释放 |
只看到“有一个对象包着另一个对象”不能证明享元或命令模式;只看到多个回调也不能证明责任链。
2. 请求封装
字段承担什么
源码文件:frameworks/base/core/java/android/os/Message.java
public long when;
Bundle data;
Handler target;
Runnable callback;
Message next;
volatile int flags;when 决定消息何时具备被取出的条件,target 指向消费它的 Handler,callback 是 Runnable 路径,next 是队列链接,flags 记录 in-use、异步和移除状态。它们共同表示一次投递的状态, 而不是一个业务对象的永久属性。
源码文件:frameworks/base/core/java/android/os/Handler.java
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
onBeforeEnqueue(queue, msg, uptimeMillis);
return queue.enqueueMessage(msg, uptimeMillis);
}真正的目标 Handler 在入队前才写入。于是 Message.obtain(Handler, ...) 和 Handler.post() 都可以预先填充部分字段,但最终的队列 owner 仍由 enqueueMessage() 确认。workSourceUid 和异步标志也在这个边界生效,消费者后续才能据此恢复工作来源或绕过同步屏障。
接口边界
经典 Command 通常有统一的执行接口,使调用者只保存一个可执行对象。Android 的 Message 没有 这样的接口;它可以携带 what/arg1/arg2/obj/data,也可以携带 Runnable,最终由 target Handler 解释这些字段:
源码文件:frameworks/base/core/java/android/os/Message.java
public Handler getTarget() {
final Handler ret = target;
return ret == NULL_HANDLER ? null : ret;
}
public Runnable getCallback() {
final Runnable ret = callback;
return ret == NULL_RUNNABLE ? null : ret;
}因此更准确的说法是“Message 是带投递元数据的请求封装,Runnable 路径具有命令对象倾向”。 把 what 的整数分支直接称为 Command 模式,会遗漏 Handler.Callback、消息时间和队列状态。
主线
下面的时序只画出请求从创建到回收的关键 owner 转移。Runnable 被封装在 Message 内, 并不会在 post() 调用点执行。
3. 队列所有权
多生产者
Handler 可以被多个线程持有;每个线程都能调用 post() 或 sendMessage()。但这不代表 多个线程共同消费队列。入队时,MessageQueue 用自身锁保护链表、尾指针、异步计数和退出状态:
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
synchronized (this) {
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
if (mQuitting) {
msg.recycle();
return false;
}
msg.markInUse();
msg.when = when;
// 按 when 插入 mMessages 链表
}
return true;
}这里有三个容易混淆的时刻:
Handler.enqueueMessage()写入target,但 Message 还没有成为队列中的节点。MessageQueue.enqueueMessage()在锁内检查isInUse、写入when并插入链表。- 返回
true只表示节点被接受,不表示 Looper 已经取出或 Runnable 已经完成。
单消费者
源码文件:frameworks/base/core/java/android/os/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.");
}
me.mInLoop = true;
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
final int thresholdOverride = getThresholdOverride();
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}loop() 只在创建它的线程上运行;MessageQueue.next() 由这个 Looper 调用,取出的 Message 再由同一个线程执行 target.dispatchMessage()。因此可以把它概括为“多生产者、单 Looper 消费者”, 但不能把它简化成普通 FIFO:同步屏障会跳过同步消息,延迟消息按 uptimeMillis 生效,文件描述符 事件和 IdleHandler 也属于同一消息循环的工作。
屏障改变什么
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}target == null 在这里表示同步屏障节点,而不是“没有消费者的普通消息”。Looper 仍然是唯一 消费者,但它在出队前改变了可见消息集合。由此可见,生产者—消费者只是并发所有权的第一层 描述,不能代替队列本身的时间和屏障规则。
4. 单线程消费
出队条件
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
final long now = SystemClock.uptimeMillis();
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
return msg;
}
}消息到期之前,native poll 使用剩余时间等待;到期后才从链表摘下并交给 Looper。这个时间 基准是 SystemClock.uptimeMillis(),深度休眠会影响它的推进方式。读者在解释“延迟 100ms”时, 不能把它改写成“100ms 后一定开始执行”:前面还有已经到期的消息、屏障、Looper 当前正在执行 的回调和线程调度。
生效顺序
消息生效顺序可拆成四个边界:
| 边界 | 所有者 | 结果 |
|---|---|---|
| 创建 | Message | 字段可修改,尚未投递 |
| 入队 | MessageQueue | isInUse、when 和链表关系生效 |
| 出队 | Looper | Message 离开队列,进入 Handler 分发 |
| 回收 | Looper/MessageQueue | 字段清理,可能进入全局池 |
“发送成功”“开始执行”“执行完成”是三个不同事件,不能用一个布尔返回值替代。
5. 分发选择
固定优先级
源码文件:frameworks/base/core/java/android/os/Handler.java
public void dispatchMessageImpl(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}这不是动态链接的责任链,而是固定的选择树:
- Message 有
callback时,直接运行 Runnable。 - 没有 callback 且 Handler 有
mCallback时,调用它;返回true终止后续处理。 - 前两条没有完成处理时,调用 Handler 的
handleMessage()。
三个节点的调用关系由类实现写死;调用者不能在运行时把一个新的 Handler 插到两者之间。 因此可以借用“责任分层”来帮助记忆,但不应宣称它实现了通用 Chain of Responsibility。
消费者是谁
target 决定哪个 Handler 消费消息,callback 决定 Handler 内部的第一条分支,what 等字段 则由 handleMessage() 的具体实现解释。相同的整数 what 在不同 Handler 中可以有不同含义, 因为消息码的命名空间属于 Handler 的消费者,而不是 MessageQueue。
6. 回调订阅
空闲回调
IdleHandler 更接近“队列状态变化时的订阅”,但它也有明确的注册表和清理语义。
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
public void addIdleHandler(@NonNull IdleHandler handler) {
if (handler == null) {
throw new NullPointerException("Can't add a null IdleHandler");
}
synchronized (this) {
mIdleHandlers.add(handler);
}
}
public interface IdleHandler {
boolean queueIdle();
}next() 只有在队列为空,或队头消息尚未到期时,才把当前注册表复制到快照数组;回调返回 false 会被移除,返回 true 才保留:
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null;
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}这个快照防止回调执行期间直接遍历正在变化的注册表,也解释了为什么新注册的 IdleHandler 不会插入当前快照。它不是“消息完成通知”:一个队列可能仍有未来消息,回调也可能抛异常,且 queueIdle() 的返回值决定监听器寿命。
全局观察
Looper.Observer 也不是 Message 的观察者列表。Looper 在每次 dispatch 开始时读取一个静态 Observer 引用,并用返回的 token 配对完成或异常通知:
源码文件:frameworks/base/core/java/android/os/Looper.java
final Observer observer = sObserver;
Object token = null;
if (observer != null) {
token = observer.messageDispatchStarting();
}
try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
}观察发生在 Looper 的消费边界,不拥有 Message,也不能改变分发选择。把 Observer 说成发布 订阅可以帮助理解通知方向,但它没有每条 Message 独立的订阅者集合。
7. 对象复用
池的形状
源码文件:frameworks/base/core/java/android/os/Message.java
private static Message sPool;
private static int sPoolSize = 0;
private static final int MAX_POOL_SIZE = 50;
public static Message obtain() {
if (!MessageQueue.getUseConcurrent()) {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0;
sPoolSize--;
return m;
}
}
}
return new Message();
}默认分支用全局锁保护一条 next 链表,池上限是 50;当 concurrent MessageQueue 实现开启时, obtain() 直接创建新对象。故它更准确地属于有界对象复用池,而不是无条件适用的严格享元: 池化只复用对象壳,obj、data、callback 的业务语义仍属于每次投递。
清理先于复用
源码文件:frameworks/base/core/java/android/os/Message.java
void recycleUnchecked() {
if (MessageQueue.getUseConcurrent()) {
clearReferenceFields();
markInUse();
} else {
clear();
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
}
void clear() {
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
when = 0;
target = null;
callback = null;
data = null;
}recycle() 会拒绝仍处于 in-use 状态的 Message;队列内部清理则使用 recycleUnchecked()。这两个入口共同维护“在队列中或正在分发的对象不能被外部再次修改”的 边界。清理 target/callback/obj/data 还会切断一次投递对 Handler、Runnable 和业务 payload 的引用;只讲“节省分配”而不讲清理,会漏掉复用池最重要的生命周期约束。
8. 接口适配
最小适配
源码文件:frameworks/base/core/java/android/os/HandlerExecutor.java
public class HandlerExecutor implements Executor {
private final Handler mHandler;
public HandlerExecutor(@NonNull Handler handler) {
mHandler = Preconditions.checkNotNull(handler);
}
@Override
public void execute(Runnable command) {
if (!mHandler.post(command)) {
throw new RejectedExecutionException(mHandler + " is shutting down");
}
}
}这是本文最明确的 Adapter:外部接口要求 execute(Runnable),适配器把它翻译成 Handler 的 post()。它不创建 Looper,不保存 Future,不提供 removeCallbacks() 句柄,也不负责调用 HandlerThread.quitSafely()。如果下游需要取消或等待完成,必须在适配器外保留 Runnable/token、 CountDownLatch 或其他业务协议。
边界表
| 结构 | 真正拥有的状态 | 读者可以安全推断 | 不能额外推断 |
|---|---|---|---|
| Message | 一次投递的字段和生命周期标志 | 可被队列传递、分发、回收 | 自带执行结果或取消协议 |
| MessageQueue | 链表、时间、屏障、监听器和退出状态 | 多生产者可安全入队,单 Looper 取出 | 是普通 FIFO 或公平队列 |
| Handler | Looper、队列、Callback 和分发入口 | 请求会在绑定线程消费 | post 成功即执行完成 |
| HandlerExecutor | 一个 Handler 引用 | execute 复用该 Handler 的队列 | 拥有线程、Future 或关闭流程 |
| IdleHandler/Observer | 回调注册或观察边界 | 可观察空闲或分发事件 | 能改变 Message 的 owner |
9. 失败边界
入队失败
Looper 退出后,MessageQueue.enqueueMessage() 回收 Message 并返回 false;因此 Handler.post() 和 HandlerExecutor.execute() 的失败分别表现为布尔值和 RejectedExecutionException。这是一条“尚未消费”的失败路径。
源码文件:frameworks/base/core/java/android/os/Looper.java
public void quitSafely() {
mQueue.quit(true);
}源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
mQuitting = true;
if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}
nativeWake(mPtr);quit(false) 删除全部待处理消息;quit(true) 保留已经到期的消息,删除未来消息。两者都会 唤醒等待中的 Looper,后续入队都会失败。这里的“取消”是队列级清理,不是中断一个已经在 Runnable.run() 中执行的任务。
执行抛异常
Looper 在 dispatchMessage() 抛出异常时先通知 Observer,再重新抛出;finally 仍负责恢复 ThreadLocalWorkSource、结束 trace 和停止监控计时。但正常路径末尾的 msg.recycleUnchecked() 位于这个 try/catch/finally 之后,异常重抛会绕过该行。因而不能把 Observer 的异常回调误认为吞掉异常,也不能假定异常消息仍会沿正常路径进入对象池。
取消竞态
源码文件:frameworks/base/core/java/android/os/Handler.java
public final void removeCallbacks(@NonNull Runnable r) {
mQueue.removeMessages(this, r, null);
}源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
if (n.target == h && n.callback == r
&& (object == null || n.obj == object)) {
Message nn = n.next;
n.recycleUnchecked();
p.next = nn;
continue;
}移除只遍历队列中仍可见的节点,复杂度是消息数量的线性级别;已经被 next() 摘下的 Message 不会再被这个方法找到。读者若要停止长任务,必须把“从队列移除”和“任务内部合作式停止” 分开设计。
10. 动手验证
验证入队
可以使用 HandlerThreadTest.testHandlerThread() 复述单消费者主线。
源码文件:frameworks/base/core/tests/coretests/src/android/os/HandlerThreadTest.java
th1.start();
assertTrue(th1.isAlive());
assertNotNull(th1.getLooper());
final Handler h1 = new Handler(th1.getLooper()) {
public void handleMessage(Message msg) {
assertEquals(TEST_WHAT, msg.what);
assertEquals(mLooperTid, Process.myTid());
mGotMessage = true;
synchronized (this) {
notifyAll();
}
}
};
Message msg = h1.obtainMessage(TEST_WHAT);
h1.sendMessage(msg);输入是一个新启动的 HandlerThread 和 what == TEST_WHAT 的 Message;动作是绑定该线程的 Handler 发送消息;断言是消息被消费且消费线程的 tid 等于 Looper 所在线程。它能证明绑定和 单线程消费,不能证明所有延迟、屏障、并发队列实现或设备调度时延。
验证空闲
IdleHandlerTest 直接覆盖返回值对注册寿命的影响。
源码文件:frameworks/base/core/tests/coretests/src/android/os/IdleHandlerTest.java
public boolean queueIdle() {
mCount++;
return false;
}测试先发送一个延迟消息,再注册 IdleHandler,随后由下一个消息检查 mCount == 1;另一个 测试让 queueIdle() 返回 true,并断言它被调用两次。输入分别是“只保留一次”和“持续保留” 两种返回值,动作是让队列从有消息变为空闲再重新活动,断言是回调次数。它能证明返回值 语义和空闲时机,不能证明回调顺序适用于所有 native 文件描述符事件。
验证清理
MessageQueueTest.testResetClearsMessages() 通过发送 100 条未来消息后调用 resetForTest(), 再断言队尾为空;testResetClearsFileDescriptorEventListeners() 则先写 pipe 触发回调,再 reset 后写入并断言不会再次计数。
源码文件:frameworks/base/core/tests/coretests/src/android/os/MessageQueueTest.java
for (int i = 0; i < 100; i++) {
handler.sendEmptyMessageDelayed(0, LONG_DELAY_MS);
}
resetQueue();
assertNull(queue.peekLastMessageForTest());这组输入把消息节点和 FD listener 同时放入队列,动作是 reset,断言分别观察消息和 listener 是否被清掉。它能证明测试重置会清理两类队列状态,不能把 resetForTest() 当作应用运行时 可调用的通用取消 API。
11. 模式边界
命名边界
本文得到的更稳妥结论如下:
Message是请求封装;Runnable 路径带有命令对象特征,但没有统一 Command 接口。MessageQueue是多生产者、单 Looper 消费的时间排序队列,并带同步屏障、FD listener 和空闲回调。dispatchMessageImpl()是固定优先级选择,不是动态可扩展责任链。IdleHandler和Looper.Observer是不同层次的回调订阅:前者观察队列空闲,后者观察 Looper 分发。Message.obtain/recycleUnchecked是有界对象复用池;清理字段和 in-use 标志与复用本身同等重要。HandlerExecutor是把 Handler 投递契约适配成Executor.execute的最小适配器。
可迁移部分
如果你设计自己的事件循环,可以迁移“请求携带元数据”“队列 owner 单一”“出队后再选择消费者” 这些结构,但不能直接迁移 Android 的屏障、uptimeMillis、native wake 或池上限。若需要结果、 取消、超时和关闭,必须显式增加对应状态对象;仅仅把 Runnable 放进队列不会自动获得 Future 语义。
12. 延伸阅读
- Handler 消息处理:沿
dispatchMessageImpl()继续看工作来源恢复、Observer 和异常边界。 - IdleHandler 空闲回调:专门分析空闲快照、返回值和队列活动变化。
- HandlerExecutor 适配器:从 API 适配角度阅读
HandlerExecutor的最小实现。 - 消息队列性能代价:继续分析链表插入、取消遍历、异步计数和对象复用的实际代价。
读者可以任选一个 Handler.post() 调用,先标出 Message 的 callback、target、when, 再沿 MessageQueue.next() 和 dispatchMessageImpl() 复述其生效顺序;最后将同一 Runnable 在 Looper 退出前后各投递一次,观察返回值与清理路径的差异。这个练习足以检验是否理解了 请求封装、队列所有权、消费时机和失败边界,而不需要把所有结构都套进某个模式名称。
