Handler 运行流程源码解析(七):Handler 中的 Message 创建

文章内容

本文内容整理自技术讲解视频《Handler 中的 Message 如何创建?》。

创建 Handler 消息时,new Message() 可以工作,但通常应使用 Message.obtain() 或 handler.obtainMessage()。它们会优先复用已经回收的 Message,池为空时才分配新对象。

一、先把创建方式用对

如果消息要发送给一个已知 Handler,可以直接从它取得消息:

java
Message msg = handler.obtainMessage(1);msg.arg1 = 100;msg.obj = result;boolean accepted = handler.sendMessage(msg);

handler.obtainMessage() 会设置消息的目标 Handler,但不会自动发送。也可以使用 Message.obtain(handler, 1, result) 后再投递。仅调用无参 Message.obtain() 时,消息尚未绑定目标,之后通过 Handler 发送即可完成绑定。

accepted 表示入队是否成功,不表示回调已经执行;后续退出或删除操作仍可能使消息不被处理。投递后应把消息交给框架管理,不再修改或手动回收它。

这些创建入口都使用 Message 类中的共享池,见 Message 官方文档。

二、复用减少分配,但不保证没有 GC

memory-jitter

消息频繁创建和释放时,会产生分配与垃圾回收开销。池化把一部分生命周期从“分配新对象 → 等待 GC”改成“取出已有对象 → 清理后归还”,有机会减少重复分配。

不过,内存曲线出现锯齿不一定说明异常,也不能断言每次 GC 都会导致掉帧。实际影响取决于分配速率、对象大小、运行时行为和当时的帧预算,需要用性能数据判断。

Message 本身只是容器。即使复用了它,新的 Runnable、Bundle、字符串或 obj 载荷仍可能需要分配。消息长时间堆积时,容器和载荷也会一直存活,无法靠一个空闲对象池消除。

三、消息池与消息队列是两条不同的链

池的核心字段位于 Message 类内部:

java
public static final Object sPoolSync = new Object();private static Message sPool;private static int sPoolSize = 0;private static final int MAX_POOL_SIZE = 50;

sPool 指向空闲 Message 链表的头部,sPoolSize 记录当前缓存数量,sPoolSync 保护池的并发取放。对于普通应用使用的框架 Message,它是进程内共享的池,不是每个 Handler 各有 50 条缓存,也不是跨进程共享。

要把它与 MessageQueue 区分开:

结构 保存什么 主要操作
MessageQueue 的 mMessages 已投递、等待处理的消息 按执行条件入队和取出
Message 的 sPool 已清理、可供复用的空闲对象 头部取出和头部归还

两条链会使用 Message 的 next 字段,但一个对象不能同时合法地属于二者。50 是空闲池的缓存上限,不是队列最多接收 50 条消息。

四、obtain:先取池头缓存,池空才 new

obtain-pool

无参 obtain() 集中了真正的取对象逻辑:

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

池非空时,方法摘下头节点,把 next 置空,与池链表断开;再清除标志位,让消息回到可供调用方填写的状态。整个摘链过程在池锁内完成,避免两个线程同时取走同一个节点。

池为空时,它在锁外创建新 Message。池不会预先准备好 50 个对象,也不承诺每次调用都能命中缓存。上述实现见 AOSP Message.obtain。

其他 obtain 重载主要是在拿到对象后填字段。handler.obtainMessage(...) 同样最终走到 Message 的池,没有另一套独立的复用机制。

Message.obtain(original) 也不是深拷贝整个业务对象图:例如 obj 仍然复制引用,Bundle 虽创建了新容器,也不能据此认为其中所有嵌套数据都被深拷贝。它并非完整复制每一个内部字段,不能把它当作线程隔离工具。

五、recycleUnchecked:先清内容,再尝试归还

recycle-pool

正常消息分发完成后,Looper 会调用 recycleUnchecked()。在本文版本中,核心实现如下:

java
void recycleUnchecked() {    flags = FLAG_IN_USE;    what = 0;    arg1 = 0;    arg2 = 0;    obj = null;    replyTo = null;    sendingUid = UID_NONE;    workSourceUid = UID_NONE;    when = 0;    target = null;    callback = null;    data = null;     synchronized (sPoolSync) {        if (sPoolSize < MAX_POOL_SIZE) {            next = sPool;            sPool = this;            sPoolSize++;        }    }}

清理字段有两个目的:避免下一个使用者读到旧数据,也避免空闲池长期保留 Handler、Runnable 和业务载荷的引用。被引用对象是否随后回收,还取决于其他地方是否持有它们以及 GC 的时机。

归还使用头插法,下一次取对象也从头部开始。因此,没有其他线程干扰时,最近归还的对象会优先被取出。池满后不会继续缓存当前对象;若没有其他有效引用,它之后可以被 GC 回收。

flags = FLAG_IN_USE 并不表示池里的消息仍在执行。这个标志还用于阻止已经归还的对象被重复回收或误投递;下次 obtain() 取出时才将标志清零。

池锁保护的是池的链接和计数。字段清理能够在锁外进行,依赖合法生命周期下当前消息由回收流程独占;这不是允许业务线程在回收时同时改写它。

六、发送之后,Message 的使用权交给框架

lifecycle

正常路径可以按五步理解:

  1. obtain() 取得可使用的 Message。
  2. 调用方填写消息内容并交给 Handler。
  3. MessageQueue 标记正在使用,把它加入待处理队列。
  4. Looper 取出消息并交给 Handler 分发。
  5. 正常分发完成后,框架清空字段并尝试归还池。

因此,不能把回调参数 msg 保存下来,准备稍后在其他任务中继续读取。回调返回后,同一个对象可能已经清空,甚至被另一条消息复用。需要后续处理时,应在回调内提取必要数据,并根据载荷是否可变决定是否复制。

同样,一条已发送的 Message 不能再次发送,也不要在 handleMessage() 中主动调用 msg.recycle()。仍在使用的消息受到回收检查,重复操作可能抛出异常。

七、没有被消费的消息,也可能被回收

quit-recycle

回收不仅发生在正常分发结束时。移除排队消息、清空队列和队列拒绝某些投递的路径,也会清理消息。

以前一篇讨论的退出为例:quit() 移除仍在队列中的全部消息;quitSafely() 先移除尚未到期的未来消息,再让保留的可派发到期消息继续处理。这里的先后顺序不能倒过来理解成“先处理完,再清理未来消息”。

批量清理需要先保存下一个节点,再回收当前节点:

java
Message p = mMessages;while (p != null) {    Message next = p.next;    p.recycleUnchecked();    p = next;}mMessages = null;

原因在于回收可能把 p.next 改成池链表的链接。如果先回收再从 p.next 找下一条待清理消息,就可能走到错误的链上。相关逻辑见 AOSP MessageQueue。

当生产速度长期超过消费速度时,大量 Message 留在队列里,尚未成为可复用的空闲对象,后续 obtain() 仍然可能不断分配。

八、对象池

design-patterns

课程把这个机制放在享元模式的语境下介绍,可以借此理解“减少重复创建”的动机。但描述 Message 的具体实现时,对象池更准确:对象在不同生命周期之间轮流复用,每次有明确的使用者,归还前清除状态。

经典享元强调共享可复用的内部状态,而 Message 保存的是会不断变化的消息内容,不能由多个使用者同时任意改写。把两者混为一谈,容易忽略最重要的所有权约束。线程池和连接池也各有自己的资源管理规则,不能简单等同于 Message 的清字段复用。

可以用下面的小实验观察对象复用,不发送消息,只取出并归还:

java
for (int i = 0; i < 5; i++) {    Message msg = Message.obtain();    Log.d("MessagePool", "identity=" + System.identityHashCode(msg));    msg.recycle();    // 从此不再使用本轮 msg。}

本文由 AgentWorkFlow 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Handler 运行流程源码解析(七):Handler 中的 Message 创建 · Yinchen 的博客