RunLoop 探索
0x1. 疑问一:主线程是如何接受到 touch 事件的?
一、推测
UI线程捕获 touch 事件,通知主线程,由主线程进行处理......
那么这一过程是否果真如此,还需要动手去验证。
二、验证
在 touchBegin 中打个断点,手指触摸屏幕时,通过调用栈,能看到 runloop 一些东西。所以,加了个符号断点__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
。
再次触摸时,发现断在了 UI 线程。如图:
由此看出 UI 线程这个 source1 事件还是比较可疑。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info), mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply, void *info) {
if (perform) {
*reply = perform(msg, size, kCFAllocatorSystemDefault, info);
}
asm __volatile__(""); // thwart tail-call optimization
}
2
3
4
5
6
查看源码发现,该函数内部只是调用了perform
函数,所以在调用perform
处加个断点,如图:
由于__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
内部只有一处函数调用,所以很容易确定callq *%rax
指令就是对perform
函数的调用。
同时在__CFRunLoopRun
函数中的 __CFRunLoopServiceMachPort
调用处的后一条指令添加断点。
因为此时主线程 runloop 处于休眠状态,若被唤醒,必然从__CFRunLoopServiceMachPort
的下一条指令开始执行。
当 UI 线程执行到call *%rax
时,主线程还处于休眠状态,当该指令执行完,主线程 runloop 会被唤醒,断点生效。
由源码可知,runloop 由以下这几种方式被唤醒:timer、source1、GCD 以及 wakeUpPort(对源码做了精简,只保留核心部分)
// waitSet包含:source1_port, timer_port, dispatch_port, wakeup_port
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// 通知观察者 已唤醒
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) {
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
}
handle_msg:;
if (MACH_PORT_NULL == livePort) {
// handle nothing
} else if (livePort == rl->_wakeUpPort) {
// do nothing on Mac OS
} else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
__CFArmNextTimerInMode(rlm, rl);
}
} else if (livePort == dispatchPort) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else {
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {
mach_msg_header_t *reply = NULL;
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
if (NULL != reply) {
mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL;
}
}
}
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
那么接下来就要确认下主线程的 runloop 究竟是被哪一种方式唤醒的。
首先比较容易排除的是 timer 和 GCD,那么就着重分析下是 source1 还是 wakeUpPort。
通过上述源码可以看到,唤醒 runloop 的端口(liveport)判断,是在__CFRunLoopDoObservers
之后,所以,在这之后寻找可疑汇编指令,设置断点进行验证。
通过断点调试发现,runloop 的地址存在%rbx
寄存器中,wakeUpPort 为 0x2103,而0x50(%rbx)
这条汇编指令的作用便是取出 runloop 中的 wakeUpPort 成员变量。那么%r13
中的值便是 livePort,也是 0x2103。由此可见,主线程 runloop 被唤醒,是 UI 线程给它的 wakeUpPort 端口发的消息,而并非 source1。
汇编指令cmpl 0x50(%rbx), %r13d
对应的源码为:
// %r13: livePort rl->_wakeUpPort: 0x50(%rbx)
else if (livePort == rl->_wakeUpPort) {
// do nothing on Mac OS
// 这里对应的汇编指令为 nop
}
2
3
4
5
进而可以看出上一条汇编指令testq %r13, %r13
对应的源码为:
if (MACH_PORT_NULL == livePort) { // MACH_PORT_NULL 宏展开为 0
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
// 这里对应的汇编指令为 nop
}
2
3
4
5
cmpl 0x50(%rbx), %r13d
是从 runloop 的内存地址偏移80个字节处取值,并与%r13
做比较。
testq %r13, %r13
是判断%r13
中的值是否为零,具体可参考这里。
三、结论
由上可知,唤醒 runloop 的端口为 wakeUpPort。整个流程大概是:
- 系统捕获触摸事件,通过 source1 唤醒 UI 线程;
- UI 线程处理 source1 事件,其实是给主线程 runloop 的 wakeUpPort 端口发消息(调用__CFMachPortPerform函数);
- 主线程 runloop 唤醒后,开始处理 source0,进而调用 touchBegin 方法。
wakeUpPort 端口的作用,就是用来唤醒 runloop。函数CFRunLoopWakeUp
的实现,其实就是向该端口发消息,从而唤醒 runloop 来处理事件。