runloop源码解读笔记

跟多数开发者一样,我也曾经迷惑于runloop,最初只了解可以通过runloop一些监听事件的通知来做一些事情,优化性能。关于runloop源码的基础知识,本文不做论述,可以参考众神的文章:

ibireme:《深入理解RunLoop》
sunyawang:《RunLoop系列之源码分析》
xiaoxiaobukuang:《RunLoop》


本文将对以上科普性文章之外的一些源码内容进行解读,便于日后自己和大家查阅。


p.s. 本文中代码部分均有删减,仅供参考。


runloop超时

在runloop源码的核心方法__CFRunLoopRun中,在进入核心的 do while循环之前,先使用 dispatch启动了一个计时器:

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
   uint64_t startTSR = mach_absolute_time();
   ......
   ......
   dispatch_source_t timeout_timer = NULL;
   struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
   if (seconds <= 0.0) { // instant timeout
       seconds = 0.0;
       timeout_context->termTSR = 0ULL;
   } else if (seconds <= TIMER_INTERVAL_LIMIT) {
   dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
   timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
       dispatch_retain(timeout_timer);
   timeout_context->ds = timeout_timer;
   timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
   timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
   dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
   dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
       dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
       uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
       dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
       dispatch_resume(timeout_timer);
   } else { // infinite timeout
       seconds = 9999999999.0;
       timeout_context->termTSR = UINT64_MAX;
   }

计时器的超时时间从哪里来?
这里的计时器用来做什么?
带着这两个问题我们继续看源码:

首先可以看出超时时间是根据 __CFRunLoopRun 的入参seconds计算的,而__CFRunLoopRun入参从哪里来,顺着源码可以找到 CFRunLoopRunSpecific, 再向上可以找到两处调用:

void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

同时,我们可以在头文件中看到这两个方法:

CF_EXPORT void CFRunLoopRun(void);
CF_EXPORT SInt32 CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);

从而可以知道,runloop开放了两种启动runloop的方式,

  • 一种是默认启动方式,此时不配置seconds,即未设置超时;
  • 一种是自定义启动方式,可以配置seconds的超时时间,以及其他mode等参数;

第一个问题解答完,我们再看第二个问题。
我们知道dispatch 计时器达到超时时间,会调用dispatch_source_set_event_handler_f 中配置的回调函数(也可以用dispatch_source_set_event_handler配置block),这里的回调函数是__CFRunLoopTimeout,源码如下:

static void __CFRunLoopTimeout(void *arg) {
    struct __timeout_context *context = (struct __timeout_context *)arg;
    context->termTSR = 0ULL;
    CFRUNLOOP_WAKEUP_FOR_TIMEOUT();// 没啥X用
    CFRunLoopWakeUp(context->rl);
    // The interval is DISPATCH_TIME_FOREVER, so this won't fire again
}
void CFRunLoopWakeUp(CFRunLoopRef rl) {
    ......
    ret = __CFSendTrivialMachMessage(rl->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0);
    if (ret != MACH_MSG_SUCCESS && ret != MACH_SEND_TIMED_OUT) CRASH("*** Unable to send message to wake up port. (%d) ***", ret);
    ......
}
static uint32_t __CFSendTrivialMachMessage(mach_port_t port, uint32_t msg_id, CFOptionFlags options, uint32_t timeout) {
    kern_return_t result;
    mach_msg_header_t header;
    ......
    result = mach_msg(&header, MACH_SEND_MSG|options, header.msgh_size, 0, MACH_PORT_NULL, timeout, MACH_PORT_NULL);
    if (result == MACH_SEND_TIMED_OUT) mach_msg_destroy(&header);
    return result;
}

从上面源码可以看出超时到达时,主要做的就是通过 __CFSendTrivialMachMessage再调用mach_msg发送消息,mach_msg参数已经配置了“发送模式”、“超时时间”、唤醒的端口为“rl->_wakeUpPort”,基于文章《runloop你理解对了吗》, 我们可以知道,runloop在休眠时,接收到mach发来的消息,会判断port,决定作何判断和处理:

if (MACH_PORT_NULL == livePort)
{
      CFRUNLOOP_WAKEUP_FOR_NOTHING();
}
else if (livePort == rl->_wakeUpPort)
{
      CFRUNLOOP_WAKEUP_FOR_WAKEUP();
}
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort)
{
      // 处理timer
}
else if (livePort == dispatchPort) 
{
      ......
      // 处理主线程队列中事件
      __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
      ......
}
else 
{
      ......
      // 处理Source1
      sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
      ......
}

也就是我们通过dispatch 定时器的可以将超时的消息通过mach ,唤醒runloop,然后执行else if (livePort == rl->_wakeUpPort)分支来处理超时(目前源码中CFRUNLOOP_WAKEUP_FOR_WAKEUP 只是一个空的宏定义,未做任何处理)。所以第二个问题答案就是这个计时器是用来在指定时间后唤醒runloop的。


mach_msg入参

mach_msg源码不知在何方,以下只是根据源码猜测,供参考。
我们从源码中摘录几个调用的地方:

mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
        msg->msgh_bits = 0;
        msg->msgh_local_port = port;
        msg->msgh_remote_port = MACH_PORT_NULL;
        msg->msgh_size = buffer_size;
        msg->msgh_id = 0;
  if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
  ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);
mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
mach_msg_header_t header;
    header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
    header.msgh_size = sizeof(mach_msg_header_t);
    header.msgh_remote_port = port;
    header.msgh_local_port = MACH_PORT_NULL;
    header.msgh_id = msg_id;
 result = mach_msg(&header, MACH_SEND_MSG|options, header.msgh_size, 0, MACH_PORT_NULL, timeout, MACH_PORT_NULL);

从上面三个调用可以猜测:

  • 第一个参数就发送消息内容msg,msg的结构体里定义了消息的收发两方的port及其他内容;
  • 第二个参数属于消息发送或接收的类型,通过宏定义已经定义好类型;
  • 第三个参数应该是用于接收者申请额外存储空间暂存消息,便于自己处理,不对源消息的空间有耦合;
  • 倒数第二个参数表示等待时间,如果是0表示发送或接收后立刻返回,如果是TIMEOUT_INFINITY,就阻塞地等待有消息,直到参数1对应port有消息才返回,继续执行后面的代码;

mach port

在runloop中多次提到port,比如基于port的source1就是就是休眠时候的唤醒源之一,比如休眠时监听消息 __CFRunLoopServiceMachPort也是通过port。

那么port是啥?Mach消息是在端口(Port)之间进行传递。一个端口只能有一个接收者,而可以同时有多个发送者。向一个端口发送消息,实际上是将消息放在一个消息队列中,直到消息能被接收者处理。

从源码中可见port类型是__CFPort /mach_port_name_t /mach_port_t,而mach_port_name_t就是无符号整数,也就是端口的索引值。源码中涉及到的port有几种类型:

    // 这个port就对应NSTimer;
    mach_port_t _timerPort;
    // 这个port对应主线程
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    dispatchPort = _dispatch_get_main_queue_port_4CF();
    // 这个port唤醒runloop
    if (livePort == rl->_wakeUpPort)

还记得__CFRunLoopRun方法中休眠时监听的port集合吗?

// 第七步,进入循环开始不断的读取端口信息,如果端口有唤醒信息则唤醒当前runLoop
__CFPortSet waitSet = rlm->_portSet;
...
...
if (kCFUseCollectableAllocator) 
{
    memset(msg_buffer, 0, sizeof(msg_buffer));
}

// waitSet 为所有需要监听的port集合, TIMEOUT_INFINITY表示一直等待
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

而这里的waitSet就是__CFPortSet ,也就是port的集合,那么__CFPortSet是什么类型呢?这个集合涉及哪些操作呢?

typedef mach_port_t __CFPortSet;
...
...
CF_INLINE kern_return_t __CFPortSetInsert(__CFPort port, __CFPortSet portSet) {
    if (MACH_PORT_NULL == port) {
        return -1;
    }
    return mach_port_insert_member(mach_task_self(), port, portSet);
}

也就说__CFPortSet的类型也是mach_port_t,即无符号整数。那么__CFPortSetInsert操作猜测应该就是按bit位来操作,不同bit位表示不同的port类型。__CFRunLoopServiceMachPort的参数入参__CFPort类型自然也可以传入waitSet,在其内部遍历各个bit位来监听各个port的消息。

另外,runloop休眠阶段的轮询的port集合是如何确定的呢?通过源码发现,正是__CFRunLoopFindMode方法中将各个port插入到waitSet中的:

static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create)
 {
    ...
    ...
    kern_return_t ret = KERN_SUCCESS;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    rlm->_timerFired = false;
    rlm->_queue = _dispatch_runloop_root_queue_create_4CF("Run Loop Mode Queue", 0);
    mach_port_t queuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
    if (queuePort == MACH_PORT_NULL) CRASH("*** Unable to create run loop mode queue port. (%d) ***", -1);
    rlm->_timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, rlm->_queue);
    
    __block Boolean *timerFiredPointer = &(rlm->_timerFired);
    dispatch_source_set_event_handler(rlm->_timerSource, ^{
        *timerFiredPointer = true;
    });
    
    // Set timer to far out there. The unique leeway makes this timer easy to spot in debug output.
    _dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 321);
    dispatch_resume(rlm->_timerSource);
    
    ret = __CFPortSetInsert(queuePort, rlm->_portSet);
    if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
    
#endif
#if USE_MK_TIMER_TOO
    rlm->_timerPort = mk_timer_create();
    ret = __CFPortSetInsert(rlm->_timerPort, rlm->_portSet);
    if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
#endif
    
    ret = __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet);
    if (KERN_SUCCESS != ret) CRASH("*** Unable to insert wake up port into port set. (%d) ***", ret);
  
    CFSetAddValue(rl->_modes, rlm);
    CFRelease(rlm);
    __CFRunLoopModeLock(rlm);   /* return mode locked */
    return rlm;
}

从上面的三个__CFPortSetInsert可以发现分别插入了queuePort、_timerPort、_wakeUpPort;另外在CFRunLoopAddSource方法中还将source1的port插入其中:

......
__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
if (CFPORT_NULL != src_port) 
{
    CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
    __CFPortSetInsert(src_port, rlm->_portSet);
}
......

再加上__CFRunLoopRun方法中加入的dispatchPort ,至此,waitSet中已经包含了可以唤醒runloop的所有port。


你可能感兴趣的:(runloop源码解读笔记)