上一篇讲了存储方案迁移,这篇回到 Android 最核心的机制之一——消息驱动模型。Handler 大家天天用,但 Looper 怎么轮询的?MessageQueue 内部什么数据结构?为什么主线程调 Looper.loop() 不会卡死?
整体架构
Android 的消息机制由四个角色组成:
Handler:发送和处理消息的入口
Message:消息载体
MessageQueue:消息队列,按时间排序
Looper:循环取消息并分发给 Handler
关系很简单:Handler 把 Message 塞进 MessageQueue,Looper 不断从队列取消息,取到后回调 Handler 的 dispatchMessage。
Looper 为什么不会卡死主线程
这个问题被问烂了,但答案不是"因为 epoll"这么简单。
Looper.loop() 是一个死循环,没错。但它不是 busy-wait,而是阻塞式等待。底层用 epoll(Linux IO 多路复用)监听一个文件描述符(eventfd 或 pipe),没有消息时线程挂起,CPU 基本不占用。有新消息入队时会唤醒线程。
所以主线程 "不卡死" 的本质是:它在等消息,有事干的时候被唤醒执行,没消息的时候休眠。这跟服务器 epoll 模型一个道理。
MessageQueue 的数据结构
很多人以为 MessageQueue 是一个普通队列(FIFO),其实它是按 when(触发时间)排序的单链表。插入新消息时从头遍历找到合适位置插入,保证链表按时间升序排列。
// 简化的 enqueueMessage 逻辑
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
Message p = mMessages;
if (p == null || when == 0 || when < p.when) {
// 新消息排在最前面
msg.next = p;
mMessages = msg;
} else {
// 遍历找到插入位置
Message prev;
do {
prev = p;
p = p.next;
} while (p != null && when >= p.when);
msg.next = p;
prev.next = msg;
}
}
}
链表而不是堆的原因:Android 消息处理需要频繁插入、删除和遍历,链表在插入时只需要改指针,不需要调整整棵树。
Message 的对象池
Message.obtain() 比 new Message() 好在哪?Message 内部维护了一个大小上限为 50 的链表对象池。用完的 Message 会被回收到池里,下次 obtain 直接复用,避免频繁创建对象和 GC。
// 推荐写法
val msg = Message.obtain(handler) {
// 处理逻辑
}
handler.sendMessage(msg)
// 不推荐
val msg = Message()
// 用完就丢,GC 压力大
用完的 message 在被分发后会自动调用 recycleUnchecked() 回收到池中。但你手动 obtain 的 message 如果没被发送,记得手动 recycle。
Handler 的 dispatchMessage 优先级
Handler.dispatchMessage 的分发顺序不是简单地调 handleMessage:
如果 Message 有
callback(Runnable),直接执行 callback.run()如果 Handler 设了
mCallback(Handler.Callback 接口),调 callback.handleMessage以上都不处理,才调 Handler 的 handleMessage
// 方式一:post Runnable
handler.post { /* 高优先级 */ }
// 方式二:Handler.Callback
val handler = Handler(Looper.getMainLooper()) { msg ->
// 中优先级
true
}
// 方式三:继承 Handler
class MyHandler : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
// 低优先级
}
}
IdleHandler:利用空闲时间
MessageQueue 有个 IdleHandler 接口,在消息队列空闲时回调:
Looper.myQueue().addIdleHandler {
// 队列空闲时执行,比如延迟初始化
doSomeLazyInit()
false // 返回 false 表示只执行一次,true 表示每次空闲都执行
}
适合做启动后的一些低优先级初始化工作,比如预加载、日志上报、缓存清理。
子线程创建 Handler 的正确姿势
// 错误写法:直接 new Handler 会崩
// java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare()
// 正确写法一:手动 prepare
thread {
Looper.prepare()
val handler = Handler()
Looper.loop()
}
// 正确写法二:HandlerThread
val handlerThread = HandlerThread("bg-handler")
handlerThread.start()
val handler = Handler(handlerThread.looper)
// 用完后 handlerThread.quit()
HandlerThread 封装了 prepare + loop,适合需要长时间运行消息循环的子线程场景。
与协程的关系
Kotlin 协程在 Android 上的 Dispatchers.Main 本质上就是复用了主线程的 Looper。Dispatchers.Main 的底层实现是 HandlerDispatcher,把协程的续体包装成 Message 发到主线程 Looper。
// 这两个效果类似
Handler(Looper.getMainLooper()).post { doSomething() }
scope.launch(Dispatchers.Main) { doSomething() }
区别是协程版支持挂起,且不需要手动管理线程切换。
小结
Android 的消息机制本质是一个事件循环 + 时间排序链表 + epoll 阻塞唤醒。理解了这个模型,Handler、AsyncTask(已废弃但原理一样)、协程 Main Dispatcher 的底层行为就都通了。
下一篇聊聊 Android 进程生命周期和 Low Memory Killer。