Message 对象池原理
面试重要度:⭐⭐⭐⭐
考察频率:字节 70% | 阿里 65% | 腾讯 60%
一、核心概念
1.1 定义与作用
一句话定义: Message 对象池是 Android 消息机制中采用的享元模式实现,通过维护一个静态单链表缓存已回收的 Message 对象,避免频繁创建和销毁 Message 带来的内存分配开销和 GC 压力。
为什么重要:
- Handler 消息机制是 Android 最核心的通信机制,消息发送极其频繁
- 每秒可能产生数十甚至上百条消息(UI 刷新、事件传递等)
- 频繁 new Message 会导致大量短生命周期对象,触发 GC 影响性能
- 面试考察点:享元模式实践、Android 性能优化思想、源码细节理解
设计目标:
无对象池:new Message → 使用 → GC 回收 → new Message → ...(频繁 GC)
有对象池:obtain() → 使用 → recycle() → obtain() → ...(复用对象,零 GC)
1.2 与其他概念的关系
Handler.sendMessage(msg)
↓
Message.obtain() ← 本文重点:从对象池获取
↓
enqueueMessage() → 消息入队(详见:../03-MessageQueue队列管理/)
↓
next() → 消息出队
↓
dispatchMessage() → 消息处理
↓
msg.recycleUnchecked() ← 本文重点:回收到对象池
- 消息队列:Message 在队列中流转,最终被回收到对象池
- Looper:loop() 中处理完消息后调用 recycleUnchecked()
- 本文重点:obtain() 获取、recycle() 回收、对象池数据结构
二、核心原理
2.1 工作机制
整体流程:
obtain() 获取 → 设置消息内容 → 入队等待 → 出队处理 → recycleUnchecked() 回收
对象池数据结构:
sPool (静态链表头)
↓
Message1 → Message2 → Message3 → ... → MessageN → null
│ │ │ │
next next next next
- 单链表结构,sPool 指向链表头
- 每个 Message 的 next 字段指向下一个空闲 Message
- sPoolSize 记录当前池中对象数量
- MAX_POOL_SIZE = 50,限制池大小
2.2 源码分析
2.2.1 Message 类核心字段
// Android 11 源码:frameworks/base/core/java/android/os/Message.java
public final class Message implements Parcelable {
// 消息标识
public int what;
public int arg1;
public int arg2;
public Object obj;
// 执行时间(绝对时间戳)
/*package*/ long when;
// 目标 Handler
/*package*/ Handler target;
// 回调 Runnable
/*package*/ Runnable callback;
// 链表指针(用于 MessageQueue 和对象池)
/*package*/ Message next;
// ========== 对象池相关 ==========
// 对象池链表头(静态,全局共享)
private static Message sPool;
// 对象池当前大小
private static int sPoolSize = 0;
// 对象池最大容量
private static final int MAX_POOL_SIZE = 50;
// 同步锁
private static final Object sPoolSync = new Object();
// 标记位:是否正在使用
private static final int FLAG_IN_USE = 1 << 0;
// 标记位:是否是异步消息
private static final int FLAG_ASYNCHRONOUS = 1 << 1;
// 标记字段
/*package*/ int flags;
}
源码解读:
sPool:静态变量,指向空闲 Message 链表的头节点next:复用字段,在队列中指向下一条消息,在对象池中指向下一个空闲对象FLAG_IN_USE:防止正在使用的 Message 被重复使用或回收sPoolSync:同步锁,保证多线程安全
2.2.2 obtain() - 获取消息
// Android 11 源码:frameworks/base/core/java/android/os/Message.java
/**
* 从对象池获取一个 Message,比 new Message() 更高效
*/
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
// 步骤1:从链表头取出一个 Message
Message m = sPool;
// 步骤2:更新链表头指向下一个
sPool = m.next;
// 步骤3:断开取出的 Message 与链表的连接
m.next = null;
// 步骤4:清除 IN_USE 标记(之前 recycle 时清除了,这里确保)
m.flags = 0;
// 步骤5:减少池大小计数
sPoolSize--;
return m;
}
}
// 步骤6:池为空,创建新对象
return new Message();
}
源码解读:
- 优先从对象池获取,池空时才 new
- 同步块保证线程安全
- 取出后清除 flags,确保干净的状态
- 时间复杂度 O(1)
2.2.3 obtain() 的重载方法
// 指定 Handler
public static Message obtain(Handler h) {
Message m = obtain();
m.target = h;
return m;
}
// 指定 Handler 和 Runnable
public static Message obtain(Handler h, Runnable callback) {
Message m = obtain();
m.target = h;
m.callback = callback;
return m;
}
// 指定 Handler 和 what
public static Message obtain(Handler h, int what) {
Message m = obtain();
m.target = h;
m.what = what;
return m;
}
// 复制另一个 Message
public static Message obtain(Message orig) {
Message m = obtain();
m.what = orig.what;
m.arg1 = orig.arg1;
m.arg2 = orig.arg2;
m.obj = orig.obj;
// ... 其他字段复制
return m;
}
// 完整参数
public static Message obtain(Handler h, int what, int arg1, int arg2, Object obj) {
Message m = obtain();
m.target = h;
m.what = what;
m.arg1 = arg1;
m.arg2 = arg2;
m.obj = obj;
return m;
}
2.2.4 recycle() - 回收消息
// Android 11 源码:frameworks/base/core/java/android/os/Message.java
/**
* 将 Message 回收到对象池
* 注意:调用后不能再使用该 Message
*/
public void recycle() {
// 检查是否正在使用
if (isInUse()) {
if (gCheckRecycle) {
throw new IllegalStateException("This message cannot be recycled because it "
+ "is still in use.");
}
return;
}
recycleUnchecked();
}
/**
* 内部回收方法,不检查使用状态(由系统调用)
*/
void recycleUnchecked() {
// 步骤1:清空所有字段,准备复用
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) {
// 步骤2:检查池是否已满
if (sPoolSize < MAX_POOL_SIZE) {
// 步骤3:插入到链表头部
next = sPool;
sPool = this;
// 步骤4:增加池大小计数
sPoolSize++;
}
// 池已满则丢弃,让 GC 回收
}
}
源码解读:
recycle():供外部调用,有安全检查recycleUnchecked():系统内部调用,无检查,更高效- 清空所有字段防止内存泄漏(特别是 obj、callback)
- 池满时直接丢弃,不会无限增长
- 头插法,时间复杂度 O(1)
2.2.5 isInUse() - 使用状态检查
/*package*/ boolean isInUse() {
return ((flags & FLAG_IN_USE) == FLAG_IN_USE);
}
/*package*/ void markInUse() {
flags |= FLAG_IN_USE;
}
2.2.6 Looper.loop() 中的回收调用
// Android 11 源码:frameworks/base/core/java/android/os/Looper.java
public static void loop() {
final Looper me = myLooper();
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next();
if (msg == null) {
return;
}
try {
// 分发消息
msg.target.dispatchMessage(msg);
} finally {
// 重要:消息处理完毕后回收
msg.recycleUnchecked();
}
}
}
2.3 对象池设计细节
细节1:为什么用链表而不是数组?
| 特性 | 链表 | 数组 |
|---|---|---|
| 插入/删除 | O(1) 头插法 | O(1) 但需要索引管理 |
| 内存占用 | 按需分配 | 预分配固定大小 |
| 复用 next 字段 | 可以 | 需要额外索引数组 |
| 实现复杂度 | 简单 | 较复杂 |
Message 已有 next 字段(MessageQueue 用),复用它实现对象池最高效。
细节2:MAX_POOL_SIZE = 50 的设计考量
private static final int MAX_POOL_SIZE = 50;
- 太小:复用效果差,仍然频繁 new
- 太大:占用过多内存
- 50 是经验值,覆盖大多数场景的峰值消息数
细节3:线程安全设计
private static final Object sPoolSync = new Object();
// 所有对象池操作都在同步块中
synchronized (sPoolSync) {
// ...
}
- 多个线程可能同时 obtain() 和 recycle()
- 使用独立的锁对象,不影响 Message 其他操作
细节4:FLAG_IN_USE 的作用
// 入队时标记
msg.markInUse(); // flags |= FLAG_IN_USE
// 回收时检查
if (isInUse()) {
throw new IllegalStateException(...);
}
// 获取时清除
m.flags = 0;
防止场景:
- 消息正在队列中等待处理时被 recycle()
- 同一个 Message 被多次 recycle()
边界情况:池满时的处理
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
// else: 直接丢弃,让 GC 回收
- 不会抛异常
- 不会阻塞
- 多余的 Message 自然被 GC
三、实际应用
3.1 典型场景
场景1:发送简单消息
// 推荐:使用 obtain()
Message msg = Message.obtain();
msg.what = MSG_UPDATE_UI;
msg.arg1 = progress;
handler.sendMessage(msg);
// 或者更简洁
Message msg = Message.obtain(handler, MSG_UPDATE_UI);
msg.arg1 = progress;
handler.sendMessage(msg);
场景2:发送带回调的消息
// 使用 obtain 创建带 Runnable 的消息
Message msg = Message.obtain(handler, () -> {
updateUI();
});
handler.sendMessage(msg);
// 等价于
handler.post(() -> updateUI());
场景3:复制消息
// 需要发送相同内容的消息到多个 Handler
Message original = Message.obtain();
original.what = MSG_BROADCAST;
original.obj = data;
handler1.sendMessage(Message.obtain(original));
handler2.sendMessage(Message.obtain(original));
original.recycle(); // 原始消息手动回收
3.2 最佳实践
推荐做法:
-
始终使用 Message.obtain() 而非 new Message()
// 推荐 Message msg = Message.obtain(); // 不推荐 Message msg = new Message(); -
使用 Handler 的便捷方法
// 这些方法内部使用 obtain() handler.obtainMessage(what); handler.obtainMessage(what, arg1, arg2); handler.obtainMessage(what, obj); -
不要手动调用 recycle()
// 错误:系统会自动回收 handler.sendMessage(msg); msg.recycle(); // 危险!消息可能还在队列中 // 正确:让系统处理 handler.sendMessage(msg); // Looper.loop() 会在处理完后自动调用 recycleUnchecked() -
特殊情况需要手动回收
// 场景:创建了消息但决定不发送 Message msg = Message.obtain(); if (shouldSend) { handler.sendMessage(msg); } else { msg.recycle(); // 不发送时需要手动回收 }
常见错误:
-
发送后继续使用 Message
// 错误 Message msg = Message.obtain(); msg.what = 1; handler.sendMessage(msg); msg.arg1 = 100; // 危险!消息可能已被回收或正在处理 // 正确 Message msg = Message.obtain(); msg.what = 1; msg.arg1 = 100; // 先设置完所有字段 handler.sendMessage(msg); // 最后发送 -
重复发送同一个 Message
// 错误 Message msg = Message.obtain(); handler.sendMessage(msg); handler.sendMessage(msg); // 抛出异常:This message is already in use // 正确 handler.sendMessage(Message.obtain(handler, what)); handler.sendMessage(Message.obtain(handler, what)); -
持有 Message 引用
// 错误 private Message cachedMsg; void send() { if (cachedMsg == null) { cachedMsg = Message.obtain(); } handler.sendMessage(cachedMsg); // 第二次调用会出问题 } // 正确:每次都 obtain void send() { handler.sendMessage(Message.obtain(handler, what)); }
3.3 性能优化建议
1. 避免高频创建新 Message
// 性能差:每次都 new
for (int i = 0; i < 1000; i++) {
handler.sendMessage(new Message()); // 1000 次内存分配
}
// 性能好:使用对象池
for (int i = 0; i < 1000; i++) {
handler.sendMessage(Message.obtain()); // 大部分复用
}
2. 合理使用 Handler 便捷方法
// sendEmptyMessage 内部会 obtain
handler.sendEmptyMessage(MSG_UPDATE);
// 等价于
Message msg = Message.obtain();
msg.what = MSG_UPDATE;
handler.sendMessage(msg);
四、面试真题解析
4.1 基础必答题
【高频题1】 为什么要使用 Message.obtain() 而不是 new Message()?
标准答案(30秒) : Message.obtain() 使用对象池复用 Message 对象,避免频繁创建新对象。Android 消息机制中消息发送非常频繁,每秒可能数十上百条。使用对象池可以减少内存分配和 GC 压力,提升性能。这是享元模式在 Android 中的典型应用。
深入展开:
对象池实现:
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(); // 池空才 new
}
面试官追问:
-
追问1:对象池是如何实现的?
- 答:使用静态单链表,sPool 指向链表头,MAX_POOL_SIZE=50 限制大小。obtain() 头部取出,recycle() 头部插入,时间复杂度都是 O(1)。
-
追问2:对象池是线程安全的吗?
- 答:是的。使用 synchronized(sPoolSync) 同步块保护所有池操作,sPoolSync 是专门的锁对象。
【高频题2】 Message 什么时候被回收到对象池?
标准答案(30秒) : Message 在 Looper.loop() 处理完消息后自动回收。具体是在 dispatchMessage() 执行完毕后,finally 块中调用 msg.recycleUnchecked() 将消息回收到对象池。开发者通常不需要手动调用 recycle()。
深入展开:
// Looper.loop()
for (;;) {
Message msg = queue.next();
try {
msg.target.dispatchMessage(msg);
} finally {
msg.recycleUnchecked(); // 自动回收
}
}
面试官追问:
-
追问1:什么情况需要手动调用 recycle()?
- 答:当创建了 Message 但决定不发送时,需要手动回收。例如条件判断后不发送的场景。
-
追问2:recycle() 和 recycleUnchecked() 有什么区别?
- 答:recycle() 会检查 FLAG_IN_USE,如果消息正在使用会抛异常。recycleUnchecked() 是系统内部使用,不做检查,更高效。
【高频题3】 Message 对象池的最大容量是多少?满了怎么办?
标准答案(30秒) : 最大容量是 50(MAX_POOL_SIZE)。池满时,recycleUnchecked() 不会将消息放入池中,直接丢弃让 GC 回收。这个设计保证了对象池不会无限增长占用过多内存,同时覆盖了大多数正常使用场景。
深入展开:
void recycleUnchecked() {
// ... 清空字段 ...
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) { // 检查是否已满
next = sPool;
sPool = this;
sPoolSize++;
}
// 满了就丢弃,不做任何处理
}
}
面试官追问:
-
追问1:50 这个值是怎么确定的?
- 答:经验值,覆盖大多数场景的峰值消息数。太小复用效果差,太大浪费内存。
-
追问2:如果应用消息特别多,50 够用吗?
- 答:通常够用。即使不够,只是部分 Message 无法复用需要 new,不会影响功能。而且消息处理很快,池会很快有空位。
【高频题4】 为什么 Message 用单链表实现对象池而不是数组?
标准答案(30秒) : 因为 Message 类本身已有 next 字段用于 MessageQueue 的链表结构,对象池复用这个字段实现,不需要额外空间。单链表的头插头取操作都是 O(1),且不需要预分配固定大小的数组,内存使用更灵活。
深入展开:
public final class Message {
/*package*/ Message next; // 复用字段
// 在 MessageQueue 中:指向队列中下一条消息
// 在对象池中:指向池中下一个空闲 Message
}
面试官追问:
-
追问1:next 字段在队列和对象池中如何区分使用?
- 答:通过 FLAG_IN_USE 标记。消息在队列中时 FLAG_IN_USE=1,回收到池中时清空所有字段包括 flags,此时 next 用于链接池中其他对象。
-
追问2:头插法有什么好处?
- 答:时间复杂度 O(1),且刚回收的对象最快被复用(热数据),CPU 缓存命中率更高。
【高频题5】 发送消息后还能修改 Message 的内容吗?
标准答案(30秒) : 不能,这是危险操作。发送后 Message 可能已经在队列中等待处理,或者正在被处理,甚至已经被回收到对象池。此时修改可能导致数据错乱或崩溃。应该在 sendMessage() 前设置好所有字段。
深入展开:
// 错误示例
Message msg = Message.obtain();
handler.sendMessage(msg);
msg.what = 1; // 危险!
// 正确做法
Message msg = Message.obtain();
msg.what = 1;
msg.arg1 = 2;
handler.sendMessage(msg); // 设置完再发送
面试官追问:
-
追问1:为什么发送后不能修改?
- 答:sendMessage() 只是把消息放入队列,不是同步执行。消息可能被其他线程的 Looper 取出处理,此时修改会有线程安全问题。
-
追问2:如果确实需要修改已发送的消息怎么办?
- 答:使用 removeMessages() 移除旧消息,发送新消息。或者让 Handler 持有共享数据,消息只传递通知。
4.2 进阶加分题
【进阶题1】 分析:Message 对象池是全局共享的吗?有什么影响?
参考答案:
是的,sPool 是静态变量,全进程共享一个对象池:
private static Message sPool; // static
private static int sPoolSize = 0;
private static final Object sPoolSync = new Object();
影响分析:
-
优点:
- 复用效率最大化,任何线程的 Message 都能复用
- 内存占用可控,只维护一个池
-
潜在问题:
-
多线程竞争:高并发时 synchronized 可能成为瓶颈
-
但实际影响很小:
- 同步块内操作极简单(几个指针操作)
- 50 个对象的池足够大多数场景
-
-
为什么不用 ThreadLocal?
- 每个线程一个池会占用更多内存
- 消息可能跨线程传递(子线程发送,主线程接收)
- 全局池的复用效率更高
追问:如何验证对象池确实减少了 GC?
- 答:使用 Android Profiler 的内存分析,对比使用 obtain() 和 new Message() 的内存分配次数和 GC 频率。
【进阶题2】 如果多次调用 recycle() 会发生什么?
参考答案:
分析 recycle() 源码:
public void recycle() {
if (isInUse()) { // 检查 FLAG_IN_USE
if (gCheckRecycle) {
throw new IllegalStateException("This message cannot be recycled because it "
+ "is still in use.");
}
return; // 或者直接返回
}
recycleUnchecked();
}
第一次 recycle():
- FLAG_IN_USE 被清除
- Message 放入池中
第二次 recycle():
- isInUse() 返回 false(已清除)
- recycleUnchecked() 再次执行
- Message 再次插入池头
- 导致:同一个 Message 在池中出现两次!
后果:
Message m1 = Message.obtain(); // 取出这个 Message
Message m2 = Message.obtain(); // 又取出同一个 Message!
// m1 == m2,导致数据混乱
追问:系统如何避免这个问题?
- 答:recycleUnchecked() 在清空字段时会设置
flags = FLAG_IN_USE,这样第二次 recycle() 时 isInUse() 返回 true。但这是个 hack,最好的做法是开发者不要手动调用 recycle()。
【进阶题3】 设计题:如果让你优化 Message 对象池,你会怎么做?
参考答案:
当前设计的不足:
- 全局锁可能成为高并发瓶颈
- 固定大小 50 可能不够灵活
优化方案:
方案1:分段锁
// 多个小池,按线程 ID 哈希选择
private static final int POOL_COUNT = 4;
private static Message[] sPools = new Message[POOL_COUNT];
private static int[] sPoolSizes = new int[POOL_COUNT];
private static Object[] sPoolSyncs = new Object[POOL_COUNT];
public static Message obtain() {
int index = (int) (Thread.currentThread().getId() % POOL_COUNT);
synchronized (sPoolSyncs[index]) {
// 从对应的池获取
}
}
方案2:无锁设计(CAS)
private static AtomicReference<Message> sPool = new AtomicReference<>();
public static Message obtain() {
Message m;
do {
m = sPool.get();
if (m == null) return new Message();
} while (!sPool.compareAndSet(m, m.next));
m.next = null;
return m;
}
方案3:动态大小
// 根据使用情况动态调整
private static int maxPoolSize = 50;
void recycleUnchecked() {
if (sPoolSize < maxPoolSize) {
// 放入池
}
// 定期统计,调整 maxPoolSize
}
追问:为什么 Android 没有采用这些优化?
- 答:当前设计已经足够好。消息处理是轻量操作,锁竞争很少。过度优化会增加复杂度,收益不明显。
4.3 实战场景题
【场景题】 应用中发现内存持续增长,怀疑是 Message 对象泄漏,如何排查?
问题分析:
-
可能原因:
- Handler 持有 Activity 引用导致泄漏
- 消息中 obj 字段持有大对象
- 未发送的 Message 没有 recycle
-
排查步骤:
// 1. 检查 Handler 定义
// 错误:非静态内部类
class MyActivity {
Handler handler = new Handler() { // 持有外部类引用
void handleMessage(Message msg) {}
};
}
// 正确:静态内部类 + WeakReference
class MyActivity {
static class MyHandler extends Handler {
WeakReference<MyActivity> activityRef;
// ...
}
}
// 2. 检查 obj 字段
Message msg = Message.obtain();
msg.obj = largeObject; // 可能导致大对象延迟释放
handler.sendMessageDelayed(msg, 60000); // 1 分钟后才处理
// 3. 检查未发送的 Message
Message msg = Message.obtain();
if (!shouldSend) {
// 忘记 recycle
}
解决方案:
// 1. 使用静态 Handler
private static class SafeHandler extends Handler {
private WeakReference<Activity> ref;
SafeHandler(Activity activity) {
ref = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
Activity activity = ref.get();
if (activity != null) {
// 处理消息
}
}
}
// 2. 及时清理
@Override
protected void onDestroy() {
handler.removeCallbacksAndMessages(null); // 移除所有消息
super.onDestroy();
}
// 3. 避免 obj 持有大对象
// 使用 what/arg1/arg2 传递简单数据
// 或使用 WeakReference 包装 obj
追问:
- 如何验证 Message 是否泄漏?—— 使用 LeakCanary 或 Android Profiler 的内存分析
- 对象池本身会导致内存问题吗?—— 不会,MAX_POOL_SIZE=50 限制了大小,且池中对象的 obj 等字段已清空
五、对比与总结
5.1 关键 API 对比
| API | 作用 | 使用场景 | 注意事项 |
|---|---|---|---|
| Message.obtain() | 从池获取 Message | 所有需要 Message 的场景 | 优先使用 |
| new Message() | 创建新 Message | 不推荐 | 性能差 |
| handler.obtainMessage() | 从池获取并绑定 Handler | 发送消息 | 便捷方法 |
| msg.recycle() | 手动回收到池 | 不发送的 Message | 发送后不要调用 |
| msg.recycleUnchecked() | 内部回收方法 | 系统使用 | 开发者不要调用 |
5.2 核心要点速记
一句话记忆: Message 对象池是静态单链表实现的享元模式,obtain() 头部取出,recycle() 头部插入,最大 50 个,线程安全。
3个关键点:
- 数据结构:静态单链表,复用 next 字段,MAX_POOL_SIZE=50
- 操作方式:obtain() 取出/recycle() 插入都是 O(1) 头部操作
- 使用原则:始终用 obtain(),发送后不要再操作,让系统自动回收
面试官最爱问:
- 为什么用 obtain() 而不是 new?(对象复用,减少 GC)
- 对象池怎么实现的?(静态单链表,同步锁)
- 什么时候回收?(Looper.loop() 处理完自动回收)
六、关联知识点
前置知识:
- Handler 基本概念(详见:
../01-Handler基础/) - 消息入队出队(详见:
../03-MessageQueue队列管理/)
后续扩展:
- 享元模式在其他场景的应用(RecyclerView.ViewHolder 复用)
- Android 中的其他对象池(连接池、线程池)
- 内存优化专题
相关文件:
../03-MessageQueue队列管理/01-消息入队enqueueMessage.md- 消息入队时的状态检查../03-MessageQueue队列管理/02-消息出队next().md- 消息出队后的回收时机../07-内存泄漏/- Handler 相关的内存泄漏问题