文章内容
本文内容整理自技术讲解视频《Handler 中的 Message 如何创建?》。
创建 Handler 消息时,new Message() 可以工作,但通常应使用 Message.obtain() 或 handler.obtainMessage()。它们会优先复用已经回收的 Message,池为空时才分配新对象。
一、先把创建方式用对
如果消息要发送给一个已知 Handler,可以直接从它取得消息:
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

消息频繁创建和释放时,会产生分配与垃圾回收开销。池化把一部分生命周期从“分配新对象 → 等待 GC”改成“取出已有对象 → 清理后归还”,有机会减少重复分配。
不过,内存曲线出现锯齿不一定说明异常,也不能断言每次 GC 都会导致掉帧。实际影响取决于分配速率、对象大小、运行时行为和当时的帧预算,需要用性能数据判断。
Message 本身只是容器。即使复用了它,新的 Runnable、Bundle、字符串或 obj 载荷仍可能需要分配。消息长时间堆积时,容器和载荷也会一直存活,无法靠一个空闲对象池消除。
三、消息池与消息队列是两条不同的链
池的核心字段位于 Message 类内部:
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() 集中了真正的取对象逻辑:
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:先清内容,再尝试归还

正常消息分发完成后,Looper 会调用 recycleUnchecked()。在本文版本中,核心实现如下:
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 的使用权交给框架

正常路径可以按五步理解:
obtain()取得可使用的 Message。- 调用方填写消息内容并交给 Handler。
- MessageQueue 标记正在使用,把它加入待处理队列。
- Looper 取出消息并交给 Handler 分发。
- 正常分发完成后,框架清空字段并尝试归还池。
因此,不能把回调参数 msg 保存下来,准备稍后在其他任务中继续读取。回调返回后,同一个对象可能已经清空,甚至被另一条消息复用。需要后续处理时,应在回调内提取必要数据,并根据载荷是否可变决定是否复制。
同样,一条已发送的 Message 不能再次发送,也不要在 handleMessage() 中主动调用 msg.recycle()。仍在使用的消息受到回收检查,重复操作可能抛出异常。
七、没有被消费的消息,也可能被回收

回收不仅发生在正常分发结束时。移除排队消息、清空队列和队列拒绝某些投递的路径,也会清理消息。
以前一篇讨论的退出为例:quit() 移除仍在队列中的全部消息;quitSafely() 先移除尚未到期的未来消息,再让保留的可派发到期消息继续处理。这里的先后顺序不能倒过来理解成“先处理完,再清理未来消息”。
批量清理需要先保存下一个节点,再回收当前节点:
Message p = mMessages;while (p != null) { Message next = p.next; p.recycleUnchecked(); p = next;}mMessages = null;原因在于回收可能把 p.next 改成池链表的链接。如果先回收再从 p.next 找下一条待清理消息,就可能走到错误的链上。相关逻辑见 AOSP MessageQueue。
当生产速度长期超过消费速度时,大量 Message 留在队列里,尚未成为可复用的空闲对象,后续 obtain() 仍然可能不断分配。
八、对象池

课程把这个机制放在享元模式的语境下介绍,可以借此理解“减少重复创建”的动机。但描述 Message 的具体实现时,对象池更准确:对象在不同生命周期之间轮流复用,每次有明确的使用者,归还前清除状态。
经典享元强调共享可复用的内部状态,而 Message 保存的是会不断变化的消息内容,不能由多个使用者同时任意改写。把两者混为一谈,容易忽略最重要的所有权约束。线程池和连接池也各有自己的资源管理规则,不能简单等同于 Message 的清字段复用。
可以用下面的小实验观察对象复用,不发送消息,只取出并归还:
for (int i = 0; i < 5; i++) { Message msg = Message.obtain(); Log.d("MessagePool", "identity=" + System.identityHashCode(msg)); msg.recycle(); // 从此不再使用本轮 msg。}本文由 AgentWorkFlow 生成