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
}
1
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;
            }
        }
    }
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

那么接下来就要确认下主线程的 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   
}
1
2
3
4
5

进而可以看出上一条汇编指令testq %r13, %r13对应的源码为:

if (MACH_PORT_NULL == livePort) { // MACH_PORT_NULL 宏展开为 0
    CFRUNLOOP_WAKEUP_FOR_NOTHING();
    // handle nothing
    // 这里对应的汇编指令为 nop   
} 
1
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 来处理事件。

最后更新: 7/14/2021, 10:40:22 AM