Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
// 底层实现:epoll IO 阻塞
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
//...省略消息(Message)处理
//...省略 idle handler
}
//...省略
nextPollTimeoutMillis = 0;
}
}
目录
1 Looper 中事件通知的疑问[Top]
我们知道,在 Android 中的大部分事件通知(比如 Activity 的生命周期通知)都是通过 Handler 机制来实现的。在 线程间通信模型——Handler 一文中曾提到:
也就是说,是 Handler 委托 Looper 轮询检查 MessageQueue 中是否有待处理的 Message。我们看一下 Looper.loop() 的关键代码:
我们看到,loop() 在一个死循环中不停地从消息队列中取下一个消息,看看是否有待处理的非空消息,如果没有,紧接着下次循环马上再去检查。咋一看,这样马不停蹄地轮询应该非常消耗 CPU 资源,给人的感觉是 CPU 一直被 UI 线程占用着(一般 loop() 运行在主线程中),应该会导致 ANR 才对。但是为什么我们没有看到这样的现象发生呢?
2 基于阻塞IO的事件通信机制[Top]
实际上,Looper.loop() 之所以没有引起 ANR,是因为其底层实现了阻塞IO事件通信,一个典型的阻塞IO通信模型如下图所示:
调用方(进程或线程)从用户空间通过系统调用读取内核中打开的一个文件,若文件中没有数据,则调用方让出 CPU 进入休眠,直到事件产生方写入事件到文件为止,这时内核唤醒调用方并返回通知事件。
可以看到,上述的过程即便在一个死循环当中也不会无休止地占用 CPU 资源。在操作系统原理进程生命周期管理中,我们知道当运行中的进程因为缺少资源时会挂起进入等待队列,从运行态进入阻塞态。所以调用方即便在死循环中通过系统调用访问空文件时,内核的进程管理会将其从运行态转变为阻塞态。
那么 Looper.loop() 是如何实现了阻塞 IO 的呢?在继续往下阅读之前,需要提前了解一下 阻塞IO和IO多路复用机制——select/poll/epoll、管道(pipe)原理、Linux eventfd。
3 Looper 实现阻塞IO[Top]
继续第一节的 Looper.loop() 源代码,其关键在于下面这行代码:
注意后面的注释
might block
(可能发生阻塞),所以秘密应该就隐藏在 MessageQueue.next() 方法中,我们继续看看:因为阻塞IO一定是在 native 层实现的,所以在 MessageQueue.next() 方法中我们只需要关注死循环中的一个本地方法——nativePollOnce()。该方法在 native 层通过 epoll 系统调用以阻塞的方式来向内核询问是否有新消息产生。
nativePollOnce() 的实现在 android_os_MessageQueue.cpp 中:
从代码中可以看到,其核心实现在 Looper.cpp 中的 pollOnce() 方法中。需要注意的是,Android 5.0 及之前的版本与 Android 6.0 及以后的版本中,Looper.cpp 的实现有所不同。
在 ≤Android5.0 中,Looper.cpp 采用 epoll+pipe 来实现阻塞IO事件通知;在 ≥Android6.0 中,Looper.cpp 采用 epoll+eventfd 来实现阻塞IO事件通知。
构造方法:
Looper.pollOnce()
Looper.wake()
Looper.wake() 方法一般在 java 层的 Handler 往 MessageQueue 中插入新的 Message 后通过本地方法调用。这样内核马上就能知道 pipe 中有新的消息,Looper.pollOnce() 中的 epoll_wait 系统调用就可以解除阻塞得到返回。
MessageQueue.enqueueMessage()
android_os_MessageQueue.cpp
构造方法:
Looper.pollOnce()
Looper.wake()
Looper.wake() 方法一般在 java 层的 Handler 往 MessageQueue 中插入新的 Message 后(MessageQueue.enqueueMessage() 方法)通过本地方法调用。这样内核马上就能知道 eventfd 计数器中有新的消息,Looper.pollOnce() 中的 epoll_wait 系统调用就可以解除阻塞得到返回。
MessageQueue.enqueueMessage() 方法的调用与 ≤Android5.0 中的一致,无须赘述。
抛开两个版本的实现细节,Looper 事件通知的总体流程可以抽象如下:
4 Looper 阻塞IO技术选型思考[Top]
原因是为了做到事件通知更及时。如果采用常规的线程间通信方法实现事件通知,则需要手动休眠线程,然后反复去轮询检查事件队列是否有新事件产生。但是这个休眠时间不好控制,设置短了浪费 CPU,设置长了,新事件半天得不到响应和处理。
而 select/poll/epoll + pipe/eventfd 这种跨进程通信方案的本质是让内核监控一个打开的文件的 IO 行为,通过注册事件、监听、阻塞、通知来实现的一套底层观察者模式。如果把 select/poll/epoll + pipe/eventfd 抽象成只是一个观察者模式,那么就不必限定其只能用于 IPC 了。
当然,也可以采用典型的观察者模式,通过线程间引用共享变量+锁实现同步的方案来实现事件通知(wait/notify方案)。但是这无疑引入了更多的耦合性,线程间无法做到很好的隔离,增加了实现的复杂性和出错的可能性。既然 Linux 内核已经提供了更简单更有效的通信方案,何必要重复造轮子呢?
在 IO多路复用——selct,poll,epoll 中我们总结 epoll 相对于 select/poll 的主要性能优势有两点:
很显然,在 Looper 事件通知中,监控的文件描述符数量非常少(2个或者1个),所以第一条优势并不明显。所以,更少的内存拷贝次数应该是选择 epoll 的主要原因。
关于这一点,可以参考 跨进程通信之管道 和 跨进程通信之eventfd
总结来说,主要原因有两点:
参考[Top]
android_os_MessageQueue.cpp
Looper.h
≤Android5.0 Looper.cpp
≥Android6.0 Looper.cpp