Android Handler 消息循环机制
核心角色先登场
首先,你要认识这个机制里的几个核心演员:
- Handler(处理者): 你通常直接打交道的对象。负责发送消息 (
sendMessage
) 和处理消息 (handleMessage
) - Message(消息): 一个包含描述和任意数据对象的容器。它的
what
字段是标识,obj
字段可以携带数据 - MessageQueue(消息队列): 一个单向链表,用来按时间顺序存储所有待处理的 Message。关键特性:它内部是空的(没有消息)或到达下一个消息的执行时间时,会使线程进入休眠状态以节省CPU资源
- Looper(循环器): 整个机制的心脏。它在一个线程内死循环,不断地从 MessageQueue 中取出 Message,并分发给对应的 Handler 进行处理
一个生动的比喻:快递系统
可以把它比作一个公司的快递系统:
- 线程(
Thread
)
○ → 整个公司 - Looper
○ → 公司里 24 小时不停业的、只有一个员工的快递分发中心。这个员工的工作就是不停地查看有没有新包裹 - MessageQueue
○ → 快递分发中心里的、带有自动定时功能的传送带(队列)。包裹按预计送达时间排序。如果传送带是空的,或者下一个包裹还没到预定时间,传送带就暂停(线程休眠) - Handler
○ → 公司里的各个部门(如市场部、财务部)。每个部门都有一个专属的收件地址 - Message
○ → 包裹。包裹上必须写明收件地址(target
,即哪个 Handler)和里面是什么(what
,obj
)
工作机制详解(从Java到Native)
现在,我们跟着流程,看看这套系统是如何协同工作的
第 1 步:创建 Looper 和 MessageQueue(准备快递中心)
一个普通的线程默认是没有开启消息循环的(它的 Looper 和 MessageQueue 都是 null
)。你需要手动调用 Looper.prepare()
和 Looper.loop()
来为它装备上这套机制
// 在一个子线程中
class WorkThread extends Thread {public Handler mHandler;@Overridepublic void run() {// 1. 调用 prepare()Looper.prepare(); // 2. 创建Handler,它会自动绑定到当前线程的LoopermHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {// 在这里处理消息}};// 3. 启动循环,开始处理消息Looper.loop();}
}
Looper.prepare()
(Java层):
○ 检查当前线程是否已经有一个 Looper 了(一个线程只能有一个 Looper)
○ 如果没有,就创建一个新的 Looper 对象
○ 在创建 Looper 的构造函数里,会创建一个至关重要的 MessageQueue 对象 (new MessageQueue()
)
○ 这个 Looper 对象会被存储到线程的局部变量 (ThreadLocal) 中,这样每个线程都能访问到自己独有的 Loopernew MessageQueue()
(Java层):
○ 它的构造函数做了一件关键的事:调用一个 Native 方法nativeInit()
○ 这里,我们从 Java 层进入了 Native 层 (C++/C)nativeInit()
(Native层):
○ 在 Native 层,同样会创建一个 NativeMessageQueue 对象
○ 这个 NativeMessageQueue 也会创建一个核心中的核心—— Looper (Native Looper)
○ 这个 Native Looper 利用了 Linux 系统的epoll
机制。epoll
是一种高效的 I/O 事件通知机制,它可以监控多个文件描述符(fd)上的事件
○ 在这里,Android 创建了一个事件管道 (Event FD),包括一个“读端”和一个“写端”。这个管道就是用来实现线程休眠和唤醒的
○ Native 的 Looper 会监控这个管道的读端。当没有消息或消息还没到执行时间时,线程就会在epoll_wait()
调用上进入休眠状态
至此,你的线程(公司)已经拥有了一个功能完备的“快递中心”(Looper + MessageQueue),并且中心里的“传送带”(MessageQueue)已经通过一根特殊的“能源管”(Event FD)连接到了Native层的“总控开关”(epoll)上
第 2 步:发送消息(各部门寄送包裹)
// 在任何地方,比如UI线程
Message msg = Message.obtain();
msg.what = 100;
msg.obj = "Some data";
workThread.mHandler.sendMessage(msg);
handler.sendMessage(msg)
最终会调用到MessageQueue.enqueueMessage(msg, uptimeMillis)
- 这个方法会将消息按照执行时间(
when
)的顺序插入到消息队列(链表)的正确位置 - 在插入过程中,如果发现队列之前是空的,或者新插入的消息是下一个要执行的“队头”消息,并且队列当前正处于休眠状态,那么它就需要唤醒队列
- 如何唤醒? 它调用一个 Native 方法
nativeWake(mPtr)
nativeWake()
(Native层):
○ 这个方法拿到与 Java MessageQueue 对应的 NativeMessageQueue
○ 然后通过 Native Looper,向我们在第 1 步创建的那个“事件管道”的写端写入一个数据
○ 这个写入操作会立即触发epoll
监控到事件,导致正在epoll_wait()
上休眠的线程被唤醒,继续工作
第 3 步:循环处理消息(快递中心开始工作)
Looper.loop()
(Java层):
○ 这是一个死循环for (;;)
○ 在循环内部,它调用MessageQueue.next()
来获取下一条要处理的消息MessageQueue.next()
(Java层):
○ 这个方法内部也是一个循环
○ 它首先检查是否有消息到达了执行时间
○ 如果没有消息或消息没到期:它会调用 Native 方法nativePollOnce(ptr, timeoutMillis)
,让线程进入定时休眠nativePollOnce()
(Native层):
○ 这个方法最终会调用到 Native Looper 的pollOnce()
○pollOnce()
内部又调用pollInner()
,而pollInner()
的核心就是epoll_wait()
系统调用
○ 线程就是在这里休眠的。它的休眠时间由下一个消息的执行时间决定(timeout
)。如果在休眠期间,有新的消息被enqueueMessage
(比如一个需要立即执行的消息),nativeWake
会写入数据,epoll_wait()
会立即返回,线程被唤醒
○ 如果休眠超时(即到了下一个消息该执行的时间),epoll_wait()
也会返回- 取消息与分发:
○ 一旦next()
方法返回了一个 Message(无论是被唤醒还是超时),Looper.loop()
就会拿到这个消息
○ 然后调用msg.target.dispatchMessage(msg)
。这里的msg.target
就是发送这个消息的 Handler
○ 最终,消息会一路被传递到你重写的handleMessage()
方法中,得到处理 - 回收消息:
○ 处理完后,loop()
会调用msg.recycleUnchecked()
,将消息放回全局的消息池中,实现复用,避免频繁创建对象造成GC
总结与流程图
核心思想:利用 Linux 的 epoll
机制,在无事可做时让线程进入休眠,在有新消息时能及时唤醒,从而兼顾了响应效率和资源消耗