0%

Android主线程与epoll

一则奇怪的现象

集成Matrix这个性能工具已经很久了,自认为对Matrix检测主线程卡顿的trace-canary模块已经熟悉了,但是最近一个同学的一次“抬杠”让我不知所措。
我们知道matrix官方介绍说trace-canary是利用Android主线程执行MessageQueue中的每一个Message时会在执行前打印一个日志,执行完成后再打印一个日志,记录这两个日志的时间差就可以判断我们这个Message是否执行超时,如果是,那么再进一步利用我们在每个方法中的首尾插桩计算方法的耗时,最终查找出到底是应用的哪个方法导致超时的。

原理就是这样直白,但是,这位同学测试了下在Activity的 dispatchTouchEvent 方法中直接sleep一段时间,示例代码如下:

1
2
3
4
5
6
7
8
9
10
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return super.dispatchTouchEvent(ev);
}

这样写,我们触摸这个view时,matrix检测不到这个超时,起初我怀疑是配置出了问题,但检查后没发现异常的配置,而且实验发现居然是能稳定复现的。顿时,我对Android主线程产生了疑惑,我不禁怀疑难道会有Java代码不经过Java层的Handler机制直接被执行?抱歉,还真的有!接下来,我将通过实验和查看源码来进一步了解Android应用的主线程运行逻辑。

Java代码绕过Handler执行

我们先看下主线程的Java代码绕过Handler机制时调用栈情况。Java中我们只需要抛一个异常就能看到调用栈,下面的调用栈是在用手指触摸屏幕上的这个View时Activity的 dispatchTouchEvent 中抛出的:

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
at com.msisuzney.testepoll.MainActivity.dispatchTouchEvent(MainActivity.java:41)
at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:402)
at android.view.View.dispatchPointerEvent(View.java:12768)
at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5274)
at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:5074)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4589)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4642)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4608)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4748)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4616)
at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4805)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4589)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4642)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4608)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4616)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4589)
at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7319)
at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7286)
at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7247)
at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7427)
at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:253)
at android.os.MessageQueue.nativePollOnce(Native Method)
at android.os.MessageQueue.next(MessageQueue.java:332)
at android.os.Looper.loop(Looper.java:168)
at android.app.ActivityThread.main(ActivityThread.java:6878)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)

对比我们点击一个View时使用Handler机制运行的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
at com.msisuzney.testepoll.MainActivity.click(MainActivity.java:46)
at java.lang.reflect.Method.invoke(Native Method)
at android.view.View$DeclaredOnClickListener.onClick(View.java:5640)
at android.view.View.performClick(View.java:6608)
at android.view.View.performClickInternal(View.java:6585)
at android.view.View.access$3100(View.java:785)
at android.view.View$PerformClick.run(View.java:25921)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:207)
at android.app.ActivityThread.main(ActivityThread.java:6878)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)

可以发现 dispatchTouchEvent 的调用栈中的的确确是没有Handler、Message这些类。按照我以前的想法,这时候主线程已经处于waiting状态了,等待其他线程post消息然后唤醒主线程执行,但是这里并没有执行到Java层的Handler机制的代码。这说明了唤醒主线程的方式不仅仅限于有Handler消息,在native层还存在通过其他方式唤醒主线程的逻辑,并不是我以前以为的类似 wait/notify 这么简单。

epoll的简单介绍

在此之前我知道主线程是通过epoll实现休眠、唤醒的,但是一直没去真正理解它的实现方式,这也是导致学艺不精的主要原因😭。
epoll 是 Linux 中的事件轮询(event poll)机制,是为了同时监听多个文件描述符的 I/O 读写事件而设计的,使用epoll主要使用到这几个api:

  1. 创建epoll实例,使用 epoll_create1(EPOLL_CLOEXEC) ,它返回是一个文件描述符。
  2. 操作 epoll 实例关联的文件描述符列表,使用 epoll_ctl(/epoll文件描述符/, /执行的操作(一般是ADD、REMOVE文件描述符/, /被操作的文件描述符/, /epoll_event事件/) ,这里需要注意,epoll_create1它返回了一个文件描述符,用于我们监听epoll,但这不是我们的目的,我们真正要监听的是我们通过epoll_ctl添加进来的文件描述符,这也就是我们所说的: epoll可以同时监听多个文件描述符的 I/O 读写事件
  3. 监听消息, 使用 epoll_wait(/epoll文件描述符/, /epoll_event事件列表/, /最大事件数/, /timeout/) ,这样就可以在一个线程中监听多个文件描述符了。

以上就是Android中主要用到几个与epoll相关的API。由于 epoll是Linux特有的,如果要写demo验证只能在Linux上运行哦,或者直接在Android上跑跑。

Looper.cpp中epoll的使用

创建native Looper

我们知道一个线程要想创建Java Looper,需要先调用Java层的Looper.prepare(),它会新建一个MessageQueue对象,而在MessageQueue的构造器中会调用nativeInit方法,正是它帮我们初始化了native层的Looper,Looper的构造器如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
system/core/libutils/Looper.cpp

Looper::Looper(bool allowNonCallbacks)
: mAllowNonCallbacks(allowNonCallbacks),
mSendingMessage(false),
mPolling(false),
mEpollRebuildRequired(false),
mNextRequestSeq(WAKE_EVENT_FD_SEQ + 1),
mResponseIndex(0),
mNextMessageUptime(LLONG_MAX) {
//监听Handler事件的描述符
mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));
LOG_ALWAYS_FATAL_IF(mWakeEventFd.get() < 0, "Could not make wake event fd: %s", strerror(errno));

AutoMutex _l(mLock);
//重建epoll实例
rebuildEpollLocked();
}

ebuildEpollLocked 方法新建了epoll实例:

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
void Looper::rebuildEpollLocked() {
// Close old epoll instance if we have one.
if (mEpollFd >= 0) {
mEpollFd.reset();
}

// Allocate the new epoll instance and register the WakeEventFd.
mEpollFd.reset(epoll_create1(EPOLL_CLOEXEC));
//Handler消息的唤醒描述符对应的epoll_event
epoll_event wakeEvent = createEpollEvent(EPOLLIN, WAKE_EVENT_FD_SEQ);
//添加监听Handler消息的文件描述符
int result = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, mWakeEventFd.get(), &wakeEvent);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake event fd to epoll instance: %s",
strerror(errno));
//如果有其他需要监听的文件描述符,在这里依依加入到epoll的监听列表中
for (const auto& [seq, request] : mRequests) {
epoll_event eventItem = createEpollEvent(request.getEpollEvents(), seq);

int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, request.fd, &eventItem);
if (epollResult < 0) {
ALOGE("Error adding epoll events for fd %d while rebuilding epoll set: %s",
request.fd, strerror(errno));
}
}
}

添加需要监听的文件描述符

上一节介绍了在新建native Looper时已经添加了一个监听Handler事件的文件描述符 mWakeEventFd ,Looper还提供了addFd方法,让epoll能监听更多的文件描述符:

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
int Looper::addFd(int fd, int ident, int events, const sp<LooperCallback>& callback, void* data) {
.......

Request request;
request.fd = fd;
request.ident = ident;
request.events = events;
request.callback = callback;
request.data = data;
//创建epoll_event
epoll_event eventItem = createEpollEvent(request.getEpollEvents(), seq);
auto seq_it = mSequenceNumberByFd.find(fd);
if (seq_it == mSequenceNumberByFd.end()) {
//添加到epoll监听的文件描述符列表中
int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, fd, &eventItem);
if (epollResult < 0) {
ALOGE("Error adding epoll events for fd %d: %s", fd, strerror(errno));
return -1;
}
mRequests.emplace(seq, request);
mSequenceNumberByFd.emplace(fd, seq);
} else {
.......
}
return 1;
}

查看源码发现主要有InputDispatcher等调用了它添加需要被监听的文件描述符:
tu1.png

线程开启epoll监听文件描述符

我们知道,在Java层开启Looper循环的方法是 Looper.loop() ,它最终会调用到MessageQueue的next方法,在这个方法中有一个死循环,它会调用nativePollOnce()休眠,等待新的Message,执行完了继续休眠,周而复始,复习下这段经典的代码:

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
45
46
47
48

Message next() {
......
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}

nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
........
}
.......
}
}

正是这段代码里这个鲜明的死循环让我产生了所有Java代码都必须在Handler机制下运行的错觉。那Java代码是怎么避开Handler机制运行的呢?原因就在 nativePollOnce() 中,它最终会调用到 Looper::pollInner ,我们直接看这个方法:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
int Looper::pollInner(int timeoutMillis) {

// Poll.
int result = POLL_WAKE;
mResponses.clear();
mResponseIndex = 0;

// We are about to idle.
mPolling = true;

struct epoll_event eventItems[EPOLL_MAX_EVENTS];
//Java层的nativePollOnce()最终堵在这里等待epoll监听的文件描述符有新的事件通知,返回的是事件的数量
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
//一旦走到这里,说明有新的文件描述符通知
// No longer idling.
mPolling = false;

// Acquire lock.
mLock.lock();
......
//遍历事件
for (int i = 0; i < eventCount; i++) {
const SequenceNumber seq = eventItems[i].data.u64;
uint32_t epollEvents = eventItems[i].events;
if (seq == WAKE_EVENT_FD_SEQ) {
//如果是Handler消息唤醒的,直接读这个写入的字符完事了,因为目的本来就是唤醒主线程,目的已达成,待会执行到外面Java层的死循环会去遍历MessageQueue
if (epollEvents & EPOLLIN) {
awoken();
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
}
} else {
//如果是其他文件描述符,那依依遍历它们,然后将它的事件放进mResponses中
const auto& request_it = mRequests.find(seq);
if (request_it != mRequests.end()) {
const auto& request = request_it->second;
int events = 0;
if (epollEvents & EPOLLIN) events |= EVENT_INPUT;
if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;
if (epollEvents & EPOLLERR) events |= EVENT_ERROR;
if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;
mResponses.push({.seq = seq, .events = events, .request = request});
} else {
ALOGW("Ignoring unexpected epoll events 0x%x for sequence number %" PRIu64
" that is no longer registered.",
epollEvents, seq);
}
}
}
.......

// Invoke all response callbacks.
for (size_t i = 0; i < mResponses.size(); i++) {
Response& response = mResponses.editItemAt(i);
if (response.request.ident == POLL_CALLBACK) {
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
#if DEBUG_POLL_AND_WAKE || DEBUG_CALLBACKS
ALOGD("%p ~ pollOnce - invoking fd event callback %p: fd=%d, events=0x%x, data=%p",
this, response.request.callback.get(), fd, events, data);
#endif
// Invoke the callback. Note that the file descriptor may be closed by
// the callback (and potentially even reused) before the function returns so
// we need to be a little careful when removing the file descriptor afterwards.
//回调监听其他文件描述符的回调接口
int callbackResult = response.request.callback->handleEvent(fd, events, data);
if (callbackResult == 0) {
AutoMutex _l(mLock);
removeSequenceNumberLocked(response.seq);
}

// Clear the callback reference in the response structure promptly because we
// will not clear the response vector itself until the next poll.
response.request.callback.clear();
result = POLL_CALLBACK;
}
}
return result;
}

源码阅读至此,已近基本解答了我最初的疑惑,InputManager注册了一个需要监听的文件描述符到主线程的epoll实例中,当有新的KeyEvent或者TouchEvent序列时不一定把消息封装成一个Message通知到主线程,还可以直接通过监听InputManager注册的文件描述符通知主线程去取这个输入事件,这种方式通过 response.request.callback->handleEvent() 回调到Java的dispatchEvent事件,它完全避开了Handler机制。但我们知道代码堵塞的起始点是在MessageQueue的next方法的死循环中的nativePollOnce(),由于此时主线程已经被唤醒,执行完这种dispatchEvent回调后,最终还是会走到nativePollOnce()之后的代码,所以无论主线程是哪个文件描述符唤醒的最后都会把Java层的MessageQueue遍历一遍,最后又继续在 epoll_wait 这里堵塞,周而复始(好像这次领悟的周而复始更深刻了😂)。

为什么使用epoll

想起了一个问题:为什么Android的主线程不直接使用Java的wait/notify机制?我想,通过这次阅读源码我有了新的理解,因为使用epoll可以监听多个文件描述符,进而做到能够在主线程监听更多的底层事件,并且epoll可以区分出来究竟是哪个事件,这点单单使用Java的wait/notify也做不到。

其他

Matrix的检测主线程耗时的方式是有漏洞的,它并不能完全覆盖所有input事件,不过看微信技术公众号介绍,他们已经找到了能检测的方案:Touch事件最终是通过server端的InputDispatcher线程传递给Client端的UI线程的,并且使用的是一对Socket进行通讯的。我们可以通过PLT Hook,去Hook这对Socket的send和recv方法来监控Touch事件。
期待他们开源。