Skip to content

Message字段与载荷

从 Message 字段写入者追到 Handler、Looper 和 MessageQueue 的真实消费者与失效时机。

基于android-17.0.0_r1
AndroidMessageHandlerLooper源码阅读

Message字段与载荷 ​

本文承接Message对象池和Handler消息处理。目标不是把 Message 做成字段词典,而是建立一张可追踪的关系:字段由谁写入,在哪个阶段被读取,何时被清空或不再可信。读完后,面对一个消息对象,你应该能判断它是业务载荷、调度元数据、消费者引用,还是只在特定 IPC/系统路径中有效。

1. 字段分层 ​

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

java
public int what;
public int arg1;
public int arg2;
public Object obj;
public Messenger replyTo;
public int sendingUid = UID_NONE;
public int workSourceUid = UID_NONE;
/*package*/ Bundle data;
/*package*/ Handler target;
/*package*/ Runnable callback;
/*package*/ volatile int flags;
public long when;
/*package*/ Message next;

这些字段不是同一层的数据:what/arg1/arg2/obj/data 描述业务;target/callback 描述消费者;when/flags/next 由队列和生命周期控制;replyTo/sendingUid/workSourceUid 只在对应系统或 IPC 路径中有意义。字段名相近不代表生效时机相同。

2. 业务载荷 ​

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

相关函数:getData()、peekData()、setData()、copyFrom()

java
public Bundle getData() {
    if (data == null) {
        data = new Bundle();
    }
    return data;
}

public Bundle peekData() {
    return data;
}

public void setData(Bundle data) {
    this.data = data;
}

arg1 和 arg2 适合少量整数,obj 适合一个对象,data 适合 Bundle。getData() 会懒创建 Bundle,peekData() 不会改变消息状态;因此调试代码如果只想观察是否携带 Bundle,应使用 peekData(),否则一次读取就可能产生新的载荷对象。

copyFrom() 会复制 what/arg1/arg2/obj 和 UID 字段,并对 Bundle 做 clone;它不复制 when、链表链接、target 和 callback。复制后的对象需要重新设置调度信息,不能当作原消息的完整替身。

3. 消费者引用 ​

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

相关函数:getTarget()、getCallback()、sendToTarget()

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

public void sendToTarget() {
    boolean unused = target.sendMessage(this);
}

普通消息必须有 target 才能进入 MessageQueue.enqueueMessage();callback 存在时,Handler 优先执行它。sendToTarget() 直接使用包内 target,调用前没有空值保护;它适合已经由 Handler 绑定目标的消息,不适合裸 Message.obtain()。

4. 入队写入 ​

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

相关函数:enqueueMessage()

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 不是只把消息“放进队列”:它在统一入队点写入消费者 target、工作来源 UID 和异步标志。when 由同一条发送路径传给队列并由队列写入;因此手动修改 target 或 flags 后再发送,仍要接受 Handler 和 MessageQueue 的状态校验。

5. 时间状态 ​

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

相关函数:enqueueMessage()、next()

java
// enqueueMessage()
msg.markInUse();
msg.when = when;

// next()
final long now = SystemClock.uptimeMillis();
if (now < msg.when) {
    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
}

when 是队列调度时间,不是消息创建时间。入队时写入,next() 用 uptimeMillis() 比较;取出后消息从链表摘下,when 仍可用于日志或调试,直到 clear() 在回收阶段将它归零。

6. 标志位 ​

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

相关字段:FLAG_IN_USE、FLAG_ASYNCHRONOUS、FLAG_REMOVED

java
static final int FLAG_IN_USE = 1 << 0;
static final int FLAG_ASYNCHRONOUS = 1 << 1;
static final int FLAG_REMOVED = 1 << 2;

public boolean isAsynchronous() {
    return (flags & FLAG_ASYNCHRONOUS) != 0;
}

public void setAsynchronous(boolean async) {
    if (async) {
        flags |= FLAG_ASYNCHRONOUS;
    } else {
        flags &= ~FLAG_ASYNCHRONOUS;
    }
}

FLAG_IN_USE 防止重复入队和提前回收;FLAG_ASYNCHRONOUS 让 MessageQueue.next() 在同步屏障后仍可找到消息;FLAG_REMOVED 服务于可移除消息的并发标记路径。它们共享一个 volatile flags 字段,修改一个标志不能粗暴覆盖其他状态。

7. 来源上下文 ​

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

相关函数:MessengerImpl.send()、enqueueMessage()

java
public void send(Message msg) {
    msg.sendingUid = Binder.getCallingUid();
    Handler.this.sendMessage(msg);
}

sendingUid 记录通过 Messenger Binder 入口发送消息的调用 UID;普通本地 Handler.post() 不会自动填充这个字段。workSourceUid 则在 Handler 入队时从当前线程的 ThreadLocalWorkSource 读取,Looper 分发期间再设置线程工作来源。两个 UID 都不是 target 线程 ID,也不能互相替代。

8. 屏障节点 ​

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

相关函数:postSyncBarrier()、next()

java
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
// target 保持 null,表示同步屏障

同步屏障复用 Message 的 arg1 保存 token,却故意不设置 target。所以 target == null 在普通入队消息中是错误,在屏障节点中却是控制语义;不能看到 null target 就一概认定为“损坏消息”。next() 会基于这个标记跳过同步消息查找异步消息。

9. 清理时机 ​

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

相关函数:clear()、clearReferenceFields()

java
void clear() {
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    target = null;
    callback = null;
    data = null;
    when = 0;
}

正常 Legacy 对象池回收会清掉载荷、消费者、时间和普通引用;因此 Handler 回调返回后继续读取同一个 Message,并没有稳定的字段快照保证。并发消息栈使用 clearReferenceFields() 时会写入 sentinel,保留 flags 和链表链接;这是另一种实现的移除协议,不能用普通池的字段清零规则解释它。

10. 测试复制 ​

源码文件:frameworks/base/tests/testables/src/android/testing/TestableLooper.java

相关函数:消息重排路径

java
Message newMessage = Message.obtain();
newMessage.copyFrom(message);
newMessage.setCallback(callback);
mQueueWrapper.recycle(message);
handler.sendMessageAtTime(newMessage, when);

测试框架重新安排消息时保留 callback 和调度时间,但通过新对象重新入队。这个动作展示了字段分类的实际边界:copyFrom() 复制载荷,不复制原对象的队列链接和 in-use 状态;when 需要由调用者单独保存和重新发送。

11. 动手验证 ​

bash
rg -n "what|arg1|arg2|obj|replyTo|sendingUid|workSourceUid|target|callback|when|flags" \
  frameworks/base/core/java/android/os/Message.java
rg -n "msg\.target|msg\.workSourceUid|setAsynchronous|msg\.when|msg\.what|msg\.callback" \
  frameworks/base/core/java/android/os/Handler.java \
  frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java \
  frameworks/base/core/java/android/os/Looper.java

读一条具体消息时,按“载荷 → 消费者 → 时间 → 标志 → 来源”顺序记录字段读写者;再检查 recycle() 或 clear() 是否已经发生。这样能避免把 what 当作全局枚举、把 obj 当作跨进程任意对象,或把 sendingUid 当作执行线程身份。

12. 边界说明 ​

本文没有展开 Parcelable 的完整跨进程序列化、Messenger 接口协议、MessageStack 的全部并发删除算法,也没有把字段命名约定上升为 Android 全局业务规范。固定源码能直接支持的是字段读写位置、状态转换和清理边界;具体消息含义仍由其 Handler 消费者定义。