5、Handler消息机制:Looper、MessageQueue、Handler工作原理,IdleHandler与同步屏障
Handler 机制,说白了就是 Android 里线程间通信的「快递系统」。
我刚开始做 Android 开发时,总觉得这玩意儿有点玄乎。为什么不能在子线程直接更新 UI?为什么非要 post 一个 Runnable?后来踩了几次坑才明白——这背后是一套精心设计的消息循环架构。
5.1 整体架构:三个核心角色
Handler 机制由三个组件构成:
- Looper——消息循环的发动机,负责不断从队列里取消息
- MessageQueue——消息的容器,按时间顺序排队
- Handler——消息的发送者和处理者,你用它发消息,也用它处理消息
它们的关系,我画了张图帮你理解:
5.2 Looper:消息循环的发动机
每个线程只能有一个 Looper。主线程的 Looper 在 ActivityThread 里已经帮我们准备好了。
你想想看,如果没有 Looper,线程执行完代码就退出了。Looper 的作用就是让线程「活」着,不断从 MessageQueue 里取消息处理。
核心方法:
Looper.prepare()——为当前线程创建 Looper(子线程必须手动调用)Looper.loop()——启动消息循环(死循环)Looper.quit()——退出循环
// 子线程使用 Looper 的标准写法
class MyThread extends Thread {
public Handler handler;
@Override
public void run() {
Looper.prepare(); // 创建 Looper 和 MessageQueue
handler = new Handler(Looper.myLooper()) {
@Override
public void handleMessage(Message msg) {
// 处理消息
}
};
Looper.loop(); // 启动循环,这行之后代码不会继续执行
}
}
注意:Looper.loop() 是一个死循环。如果不调用 quit(),线程永远不会结束。我曾经在项目里忘记在 Activity 销毁时退出子线程的 Looper,结果导致内存泄漏——Handler 持有 Activity 引用,Activity 无法被 GC 回收。
5.3 MessageQueue:消息的容器
MessageQueue 内部其实是一个单向链表。消息按执行时间排序,时间越早的越靠前。
它的核心方法是 next()——这是一个阻塞方法。如果队列里没有消息,或者所有消息都还没到时间,next() 就会阻塞住。
// MessageQueue.next() 简化逻辑
Message next() {
for (;;) {
// 如果有同步屏障,优先处理异步消息
if (mMessages != null && mMessages.target == null) {
// 找到第一个异步消息
}
if (mMessages != null) {
if (now < mMessages.when) {
// 还没到时间,阻塞
Binder.flushPendingCommands();
nativePollOnce(ptr, nextPollTimeoutMillis);
} else {
// 取出消息并返回
Message msg = mMessages;
mMessages = msg.next;
msg.next = null;
return msg;
}
} else {
// 队列为空,无限阻塞
nativePollOnce(ptr, -1);
}
}
}
这里有个细节:nativePollOnce() 是 native 层的阻塞调用。它利用 Linux 的 epoll 机制,让线程在没消息时休眠,有消息时被唤醒。这样就不会空转消耗 CPU。
5.4 Handler:消息的收发员
Handler 做的事情其实很简单:
- 发送消息——把 Message 塞到 MessageQueue 里
- 处理消息——Looper 取出消息后,回调 Handler 的 handleMessage()
Handler 发送消息有几种方式:
// 方式一:sendMessage
handler.sendMessage(msg);
handler.sendMessageDelayed(msg, 1000);
handler.sendEmptyMessage(WHAT_UPDATE);
// 方式二:post Runnable
handler.post(new Runnable() {
@Override
public void run() {
// 更新 UI
}
});
handler.postDelayed(runnable, 500);
// 方式三:obtainMessage(推荐,复用 Message 对象)
Message msg = handler.obtainMessage(WHAT_UPDATE);
handler.sendMessage(msg);
我的习惯:能用 post 就用 post,代码更简洁。但如果需要区分多种消息类型,用 sendMessage + what 字段更清晰。另外,obtainMessage 比 new Message() 好——它从全局消息池里复用对象,减少内存分配。
5.5 IdleHandler:空闲时的「摸鱼」机会
IdleHandler 是 MessageQueue 里的一个接口。当 Looper 处理完所有消息,进入空闲状态时,会回调 IdleHandler。
// IdleHandler 接口定义
public static interface IdleHandler {
boolean queueIdle();
}
// 使用示例
Looper.myLooper().getQueue().addIdleHandler(new IdleHandler() {
@Override
public boolean queueIdle() {
// 空闲时执行的任务
Log.d("TAG", "主线程空闲了,做点后台工作");
return false; // false 表示只执行一次,true 表示每次空闲都执行
}
});
我在项目里用过 IdleHandler 做两件事:
- 延迟加载——页面首帧渲染完成后,利用空闲时间加载次要资源
- 预加载数据——列表滑动停止后,利用空闲时间预加载下一页数据
避坑指南:我曾经在 IdleHandler 里做了耗时操作,导致界面卡顿。记住,IdleHandler 虽然叫「空闲」,但它仍然运行在主线程。如果任务太重,还是丢到子线程去吧。
5.6 同步屏障:让紧急消息插队
同步屏障是 MessageQueue 里一个比较「冷门」但很重要的机制。
正常情况下,消息按时间顺序排队。但有些消息需要「插队」——比如 View 的绘制请求。这时候就需要同步屏障。
// 同步屏障的工作原理
// 1. 插入一个 target 为 null 的 Message(这就是屏障)
// 2. 后续的普通消息被屏障挡住,只有异步消息能通过
// 3. 屏障被移除后,恢复正常
// 插入屏障(系统内部使用)
int token = messageQueue.postSyncBarrier();
// 移除屏障
messageQueue.removeSyncBarrier(token);
同步屏障的应用场景:
- View 绘制——当 View 需要重绘时,系统插入屏障,让绘制消息优先执行
- 输入事件——触摸事件需要及时响应,不能被普通消息阻塞
关键点:同步屏障只对异步消息有效。普通消息(同步消息)会被屏障挡住。Handler 默认发送的是同步消息,需要调用 setAsynchronous(true) 才能发送异步消息。
5.7 总结:一张表看清所有角色
| 组件 | 职责 | 关键方法 | 注意事项 |
|---|---|---|---|
| Looper | 消息循环 | prepare(), loop(), quit() | 子线程需手动调用 prepare() |
| MessageQueue | 消息存储 | next(), enqueueMessage() | 内部是链表,next() 会阻塞 |
| Handler | 收发消息 | sendMessage(), handleMessage() | 注意内存泄漏 |
| IdleHandler | 空闲回调 | queueIdle() | 不要做耗时操作 |
| 同步屏障 | 消息优先级 | postSyncBarrier() | 系统内部使用,开发者一般不用 |
Handler 机制说复杂也复杂,说简单也简单。你只要记住一句话:Handler 发消息,MessageQueue 存消息,Looper 取消息并分发。剩下的细节,都是在解决「什么时候取」「取哪个」「取不到怎么办」这些问题。
嗯,如果你能把这套机制吃透,Android 的消息驱动模型你就掌握了八成。剩下的两成,藏在源码的细节里——比如 native 层的 epoll 实现、Message 的对象池复用、屏障的 token 管理。有兴趣的话,可以自己去翻翻源码,会有更多收获。
公众号:蓝海资料掘金营,微信deep3321