Skip to content

消息机制的结构约束

从 Handler、Message、MessageQueue 和 Looper 的真实控制流出发,辨析请求封装、单消费者分发、回调选择、对象复用与接口适配之间的结构约束。

基于android-17.0.0_r1
AndroidHandlerLooperMessageQueue源码阅读

消息机制的结构约束 ​

“这段代码用了什么设计模式”不是阅读 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

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

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

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

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

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;
}

这里有三个容易混淆的时刻:

  1. Handler.enqueueMessage() 写入 target,但 Message 还没有成为队列中的节点。
  2. MessageQueue.enqueueMessage() 在锁内检查 isInUse、写入 when 并插入链表。
  3. 返回 true 只表示节点被接受,不表示 Looper 已经取出或 Runnable 已经完成。

单消费者 ​

源码文件:frameworks/base/core/java/android/os/Looper.java

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

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

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字段可修改,尚未投递
入队MessageQueueisInUse、when 和链表关系生效
出队LooperMessage 离开队列,进入 Handler 分发
回收Looper/MessageQueue字段清理,可能进入全局池

“发送成功”“开始执行”“执行完成”是三个不同事件,不能用一个布尔返回值替代。

5. 分发选择 ​

固定优先级 ​

源码文件:frameworks/base/core/java/android/os/Handler.java

java
public void dispatchMessageImpl(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

这不是动态链接的责任链,而是固定的选择树:

  1. Message 有 callback 时,直接运行 Runnable。
  2. 没有 callback 且 Handler 有 mCallback 时,调用它;返回 true 终止后续处理。
  3. 前两条没有完成处理时,调用 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

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

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

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

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

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

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 或公平队列
HandlerLooper、队列、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

java
public void quitSafely() {
    mQueue.quit(true);
}

源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java

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

java
public final void removeCallbacks(@NonNull Runnable r) {
    mQueue.removeMessages(this, r, null);
}

源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java

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

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

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

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.post() 调用,先标出 Message 的 callback、target、when, 再沿 MessageQueue.next() 和 dispatchMessageImpl() 复述其生效顺序;最后将同一 Runnable 在 Looper 退出前后各投递一次,观察返回值与清理路径的差异。这个练习足以检验是否理解了 请求封装、队列所有权、消费时机和失败边界,而不需要把所有结构都套进某个模式名称。