Looper探底

之前有整理过一篇Android Handler Looper机制,不过细细想来,其实还是可以再探探底的。

为什么主线程的Looper,”死循环”取消息并不会引发ANR?

因为ANR是主线程中需要处理的各类用户输入,得不到及时响应才被触发。
因此即便Looper中只要在有消息需要响应的时候,及时被唤醒响应,便不会存在该问题。

Looper是怎么取消息的,采用什么策略?

这个其实在我的这篇文章中有分析到,如下图:

其取消息是通过MessageQueue#next()获取的,其出队策略并非始终从队头出。

首先MessageQueue的消息在入队的时候,会根据消息所带参数when的所代表的时间(单位为毫秒),进行入队并保持队列按照when进行升序排列;

而后出队是与是否存在Barrier消息有关:

  • 如果存在BarrierBarrier之前的任何消息,按照时间升序逐一出队;Barrier之后的消息,只有isAsynchronous()true的消息才能够按照时间按升序逐一出队
  • 如果不存在Barrier,任何消息,按照时间升序逐一出队

什么是Barrier消息,在Android中有什么应用?

Barrier消息是Message#target的值为空的消息,通常对上层是隐藏的,我们可以看到Handler带有async的构造方法是@hide的,而MessageQueue#postSyncBarrierMessageQueue#removeBarrier都是@hide的,而Message#setAsynchronous也是在API22之后才对外开放。

Looper中是怎样进行休眠的,何时被唤醒?

休眠过程:

1
2
3
4
5
6
7
8
9
// MessageQueue.java
Message next() {
...
for(;;) {
...
nativePollOnce(ptr, nextPollTimeoutMillis)
...
}
}
1
2
3
4
5
6
7
// android_os_MessageQueue.cpp
// https://android.googlesource.com/platform/frameworks/base/+/master/core/jni/android_os_MessageQueue.cpp
void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
...
mLooper->pollOnce(timeoutMillis);
...
}
1
2
3
4
5
// Looper.h
// https://android.googlesource.com/platform/frameworks/native/+/jb-dev/include/utils/Looper.h
inline int pollOnce(int timeoutMillis) {
return pollOnce(timeoutMillis, NULL, NULL, NULL);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Looper.cpp
// https://android.googlesource.com/platform/frameworks/native/+/jb-dev/libs/utils/Looper.cpp
...
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
int wakeFds[2];
int result = pipe(wakeFds); // 创建管道
...
mWakeReadPipeFd = wakeFds[0]; // 管道的读端文件描述符
mWakeWritePipeFd = wakeFds[1]; // 管道的写端文件描述符
...
// Allocate the epoll instance and register the wake pipe.
mEpollFd = epoll_create(EPOLL_SIZE_HINT); // 创建epoll实例并获得其文件描述符
...
result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem); // 开始监控管道读端事件
...
}
...
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
int result = 0;
for (;;) {
...// 由于outFd,outEvents, outDAta
result = pollInner(timeoutMillis);
}
}
...
int Looper::pollInner(int timeoutMillis) {
...
// 进入等待epoll的事件通知
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
...
}

唤醒过程:

1
2
3
4
5
6
7
// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
...
if (needWake) {
nativeWake(mPtr);
}
}
1
2
3
4
5
// android_os_MessageQueue.cpp
// https://android.googlesource.com/platform/frameworks/base/+/master/core/jni/android_os_MessageQueue.cpp
void NativeMessageQueue::wake() {
mLooper->wake();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Looper.cpp
// https://android.googlesource.com/platform/frameworks/native/+/jb-dev/libs/utils/Looper.cpp
void Looper::wake() {
ssize_t nWrite;
do {
nWrite = write(mWakeWritePipeFd, "W", 1); // 在管道写端写入一个新的字符'W',让其可读
} while (nWrite == -1 && errno == EINTR);
if (nWrite != 1) {
if (errno != EAGAIN) {
ALOGW("Could not write wake signal, errno=%d", errno);
}
}
}
// 前面休眠最后到这个方法
int Looper::pollInner(int timeoutMillis) {
...
// 等待epoll的事件通知,由于写入了新的内容,因此被唤醒并返回可读事件数目
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
...
for (int i = 0; i < eventCount; i++) {
...
if (fd == mWakeReadPipeFd) { // 是管道的读端被唤醒
if (epollEvents & EPOLLIN) { // 是可读事件
awoken(); // 到这里是已经唤醒了,因此只是将管道中的数据读完,也就消费完可读事件
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake read pipe.", epollEvents);
}
} else { // 不是管道的读端被唤醒
...
}
}
...
}
...
void Looper::awoken() {
char buffer[16];
ssize_t nRead;
do {
nRead = read(mWakeReadPipeFd, buffer, sizeof(buffer));
} while ((nRead == -1 && errno == EINTR) || nRead == sizeof(buffer));
}

什么是I/O多路复用?

也被称为”事件驱动”,如Looper中采用的epoll系统调用函数来使用该功能,休眠与唤醒当前Looper所在线程。

除了epoll外还有selectpollkqueue等函数可以用来实现I/O多路复用,这里的”复用”指的是复用同一个线程,因为正常的I/O操作,是会阻塞住当前线程直到缓冲区中有数据,因此会霸占整个线程,而I/O多路复用则是通过同时监听多个描述符的读写就绪情况,配合非阻塞的socket使用时,每次都是系统通知描述符可读时,才去执行有效的read操作,让多个描述符的I/O操作都能够在一个线程内并发交替地顺序完成。


更多疑惑,欢迎评论讨论。


Jacksgong wechat
欢迎关注Jacks Blog公众号,第一时间接收原创技术沉淀干货。