Skip to content

消息队列性能代价

从入队排序、扫描式取消、消息对象池和异步计数源码,定位消息队列的真实成本与优化边界。

基于android-17.0.0_r1
AndroidMessageQueueHandlerMessage源码阅读

消息队列性能代价 ​

消息队列优化不能从“少发消息”一句话开始。Android 17 的 Legacy MessageQueue 在入队时按 when 扫描链表,在 removeCallbacks/hasMessages 时也可能扫描整个队列;Message 对象池 只减少分配,不改变锁、排序和分发成本;异步消息还维护 mAsyncMessageCount 以优化屏障场景。 这些成本必须和 Handler 的生命周期、取消语义及消息处理时间一起分析。

本文面向已经读过 MessageQueue消息入队、 消息延迟边界、消息泄漏排查 和 Looper消息监控 的读者。本文限定 Android 17 Legacy 实现,不 复用归档中的吞吐数字、内存数字、Concurrent Queue 假设或固定 ROI 排名;重点说明哪些操作在 源码中确实是 O(n)、哪些优化会改变语义,以及如何用计数/实验验证而不是凭感觉判断。

读完后,读者应能定位一次投递的队列成本,判断 hasMessages() 去重是否可能反而扫描队列, 区分 Message 池化与消息合并,解释 postAtFrontOfQueue() 的公平性边界,并设计一个不依赖 内部吞吐数字的对照实验。

1. 入队结构 ​

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

Legacy 队列用按 when 排序的链表保存消息。新消息通常需要从队头向后寻找插入点;只有插入 队头或空队列可以立即完成头部更新。

java
msg.markInUse();
msg.when = when;
Message p = mMessages;
if (p == null || when == 0 || when < p.when) {
    msg.next = p;
    mMessages = msg;
} else {
    Message prev;
    for (;;) {
        prev = p;
        p = p.next;
        if (p == null || when < p.when) {
            break;
        }
    }
    msg.next = p;
    prev.next = msg;
}

因此延迟消息投递成本不只有 Message.obtain():当队列很长、时间点分散、并发线程频繁入队 时,锁内链表扫描也会增加调用方等待。

2. 唤醒判断 ​

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

入队会根据新消息是否成为队头、队列是否阻塞、队头是否为同步屏障和异步计数决定是否 nativeWake()。不是每次 sendMessage 都要写 wake fd。

java
boolean needWake;
if (p == null || when == 0 || when < p.when) {
    needWake = mBlocked;
} else {
    needWake = mBlocked && p.target == null && msg.isAsynchronous();
    if (when >= mLast.when) {
        needWake = needWake && mAsyncMessageCount == 0;
    }
}

if (needWake) {
    nativeWake(mPtr);
}

减少消息数量可能减少 wake 和锁操作,但不能假设每次 wake 都是主要瓶颈;必须结合队列是否 阻塞、消息是否成为新头和 native poll 状态观测。

3. 异步计数 ​

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

队列维护 mAsyncMessageCount,入队异步消息时增加,出队或移除时减少。它用于屏障队头场景 避免每次尾部插入都重新扫描是否存在异步消息。

java
private int mAsyncMessageCount;

if (msg.isAsynchronous()) {
    mAsyncMessageCount++;
}

出队或移除时,异步计数对称递减:

java
if (msg.isAsynchronous()) {
    mAsyncMessageCount--;
}

异步标记是调度语义,不是性能开关。把普通业务消息全部标为异步可能改变屏障顺序,不能只 为了“更快”而使用。

4. 取消扫描 ​

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

Handler 的 removeCallbacks()、removeMessages() 和 hasMessages() Javadoc 都明确提示 最坏情况 O(n),并建议用外部计数器或状态代替频繁查询。

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

public final boolean hasCallbacks(@NonNull Runnable r) {
    return mQueue.hasMessages(this, r, null);
}

如果高频路径每次都先 hasMessages() 再 sendMessage(),一次业务事件可能进行一次完整扫描 再进行一次入队扫描;同时检查和发送之间仍有并发窗口,不能把它当成原子去重。

5. 引用匹配 ​

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

精确移除通过 Handler、what、callback 和可选 obj 匹配;普通 API 使用引用相等。按 equals 匹配 的隐藏 API 另有警告,因为错误 equals 可能删除无关消息。

java
if (n.target == h && n.callback == r
        && (object == null || n.obj == object)) {
    n.recycleUnchecked();
    p.next = n.next;
}

使用稳定 token 可以缩小清理范围;使用 null token 会匹配 Handler 下所有相关 callback/message, 清理范围大但扫描仍是 O(n)。

6. 消息池化 ​

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

Message.obtain() 优先从静态对象池取出;recycleUnchecked() 清理字段后把 Message 放回池。 池化减少短生命周期 Message 的分配压力,但不会消除入队链表、锁和 Handler dispatch 成本。

java
public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0;
            return m;
        }
    }
return new Message();
}

处理完成后,Message 才进入清理和对象池路径:

java
void recycleUnchecked() {
    clear();
    synchronized (sPoolSync) {
        if (sPoolSize < MAX_POOL_SIZE) {
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }
}

应使用 obtainMessage() 保持字段初始化和回收契约;不要把对象池大小当作可调的业务缓存, 也不要保留已 recycle 的 Message 引用。

7. 消息合并 ​

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

源码支持的性能推论是:每条消息都要经过 Message 创建、入队、可能 wake 和 dispatch。若业务 事件允许合并,应让一条 Message 表示“有工作需要处理”,把具体数据放到受控集合中,并在 消费后清空/重置预约标志。

java
if (!mUpdateScheduled) {
    mUpdateScheduled = true;
    mHandler.sendEmptyMessage(MSG_UPDATE);
}

// handleMessage
mUpdateScheduled = false;
drainPendingUpdates();

这不是 Handler 自带的合并语义;合并集合的锁、数据顺序和取消规则由业务 owner 负责。若每个 事件都必须独立确认,则不能为了减少消息而丢弃或合并它们。

8. 队头插入 ​

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

postAtFrontOfQueue() 使用 when == 0 让消息成为队头。Javadoc 明确警告它可能造成饥饿、 排序问题和其它副作用。

java
public final boolean postAtFrontOfQueue(@NonNull Runnable r) {
    return sendMessageAtFrontOfQueue(getPostMessage(r));
}

public final boolean sendMessageAtFrontOfQueue(@NonNull Message msg) {
    return enqueueMessage(queue, msg, 0);
}

它适合明确的控制路径,不适合把高频业务更新都放到队头。队头优先不等于打断当前 callback, 也不等于更低总延迟。

9. Handler 统计 ​

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

Handler 提供 dump()/dumpMine(),可以把 Looper 和队列状态输出到 Printer;dumpMine() 只 过滤当前 Handler 的消息。它比频繁 hasMessages() 更适合一次性诊断,但输出本身也会遍历 队列并产生 I/O 成本。

java
public final void dumpMine(@NonNull Printer pw, @NonNull String prefix) {
    pw.println(prefix + this + " @ " + SystemClock.uptimeMillis());
    if (mLooper == null) {
        pw.println(prefix + "looper uninitialized");
    } else {
        mLooper.dump(pw, prefix + "  ", this);
    }
}

性能优化前先用 dump、Looper slow log 或 Observer 找出真实队列/dispatch 问题,再决定是否合并 消息;不要用微优化替代定位。

10. 处理批次 ​

源码文件:frameworks/base/core/java/android/view/ViewRootImpl.java

ViewRootImpl 的输入路径展示了批处理:它把多个 QueuedInputEvent 保存在自己的队列中, doProcessInputEvents() 一次排空当前可处理事件,而不是每个 native input 都新增一次复杂 traversal。

java
while (mPendingInputEventHead != null) {
    QueuedInputEvent q = mPendingInputEventHead;
    mPendingInputEventHead = q.mNext;
    mPendingInputEventCount--;
    deliverInputEvent(q);
}

批处理只有在事件可以合并或顺序仍然可保持时才成立;输入事件语义要求按接收顺序,不能照搬 到所有业务消息。

11. 线程选择 ​

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

HandlerThread 的 Javadoc 明确提醒线程资源、MessageQueue 锁竞争和优先级反转。为每种小任务 创建独立 HandlerThread 可能增加空闲线程和队列锁;是否改用 Executor 必须结合任务并发模型。

java
/**
 * Use this class only if you must work with Handler API and need a new Looper thread.
 * Otherwise prefer Executor or ExecutorService.
 */
public class HandlerThread extends Thread {
    // 每个实例都是一个真实 Thread
}

一个 HandlerThread + Handler 能保留串行顺序;Executor pool 能提供并发和更丰富的 Future 语义。 不能把“减少线程”与“减少消息”混成同一个优化目标。

12. 并发队列边界 ​

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

Android 17 的 MessageQueue 存在不同实现/feature flag 路径;本文结论只覆盖已核对的 Legacy 实现。不能根据归档文章直接断言所有应用默认使用某个 ConcurrentSkipListSet 或某个系统属性。

text
当前文章适用:LegacyMessageQueue.enqueueMessage/remove/next
需要另行核对:CombinedMessageQueue、CombinedDeliMessageQueue 及其 feature flag

性能比较必须同时登记实现分支、flag、线程条件和测试输入,否则“并发队列更快”没有可复现 边界。

13. 验证输入 ​

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

第一组对照测量投递固定数量消息:单条投递、业务合并、hasMessages() 去重三种方案,分别 记录入队线程耗时、Looper 消费完成时间和消息处理次数。不要只测 sendMessage 调用耗时。

第二组验证取消扫描:构造不同长度队列,在队头/中部/尾部移除 callback,记录扫描次数或总耗时; 用外部预约布尔值与 hasMessages() 方案比较,确认外部状态减少查询而不改变取消语义。

第三组验证对象池:在消息处理完成后确认 Message 的引用字段已清理,再重复 obtain;该实验 证明回收字段契约,不证明对象池一定覆盖所有分配或带来固定百分比收益。

14. 复查命令 ​

源码文件:

  • frameworks/base/core/java/android/os/Handler.java
  • frameworks/base/core/java/android/os/Message.java
  • frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
  • frameworks/base/core/java/android/view/ViewRootImpl.java
  • frameworks/base/core/java/android/os/HandlerThread.java
bash
rg -n "enqueueMessage|needWake|mAsyncMessageCount|for \(;;\)" \
  frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java

rg -n "removeCallbacks|removeMessages|hasMessages|O\(n\)|postAtFrontOfQueue" \
  frameworks/base/core/java/android/os/Handler.java

rg -n "obtain\(|recycleUnchecked|clear\(|MAX_POOL_SIZE|sPool" \
  frameworks/base/core/java/android/os/Message.java

rg -n "mPendingInputEventHead|doProcessInputEvents|scheduleProcessInputEvents" \
  frameworks/base/core/java/android/view/ViewRootImpl.java

15. 适用边界 ​

消息队列优化的第一原则是先区分消息数量、入队扫描、取消扫描、dispatch 时间、线程锁和对象 分配。合并/去重会改变语义,池化只影响分配,异步位只影响屏障选择,队头插入会改变公平性, HandlerThread 池化会改变线程归属;没有一种优化可以覆盖所有成本。

用源码可追踪的计数和对照实验确认变化,再决定是否修改。特别是 hasMessages()、 removeCallbacks() 和 dump 都可能是 O(n),不能把它们当作免费状态查询。