You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Messagenext() {
// 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.finallongptr = mPtr;
if (ptr == 0) {
returnnull;
}
intpendingIdleHandlerCount = -1; // -1 only during first iterationintnextPollTimeoutMillis = 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
The text was updated successfully, but these errors were encountered: