Runloop详解

188 阅读8分钟

前言

  • Runloop的 官方文档 中,我们可以看到Runloop是一个死循环模型,线程在执行完任务后会进行休眠,有新的任务需要执行时就会被唤醒。如下图所示

image.png

runloop事件类型

  • 一共有6种类型的runloop事件:
      1. block应用__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
      1. timer应用__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
      1. 响应source0__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
      1. 响应source1__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
      1. GCD主队列__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
      1. 观察源(observer)__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

while循环比较

  • 一般while
int main(int argc, char * argv[]) {
    @autoreleasepool {
        while (1) {
            NSLog(@"hello");
        }
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));;
    }
}

image.png

  • Runloop所占cpuimage.png
  • Runloop总结:
  1. 保证程序的持续运行
  2. 处理APP中的各种事件(触摸定时器performSelector
  3. 节省cpu资源、提供程序的性能:该做事就做事,该休息就息

源码解读

  • Runloop对应2套Api, 如下

image.png

  • Runloop在底层是CFRunloop,解析来我们将对CFRunloop进行分析,即对CoreFoundation源码CFRunloop.c文件进行阅读

Runloop的创建

  • CFRunLoopRun方法
void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
    // -   主要是通过判断`result`的结果进行`do-while`循环,说明这里是`runloop`创建的过程
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
  • 看看CFRunLoopGetCurrent,它是获得CFRunLoopRef的函数
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    // 根据类型从`TSD`获取`rl`,如果存在则返回,如果不存在则根据当前线程从`_CFRunLoopGet0`函数获取
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
  
    return _CFRunLoopGet0(pthread_self());
}
  • _CFRunLoopGet0函数

image.png

  • 获取CFRunLoopRef的过程比较简单:
    • 1.先判断当前线程,如果不存在则设置为主线程
    • 2.判断CFMutableDictionaryRef是否存在:
      • 不存在:创建CFMutableDictionaryRef,然后在主线程创建CFRunLoopRef,然后以主线程为keymainloopvalue存入字典
      • 存在:根据当前线程获取CFRunLoopRef,如果没有获取到,说明当前线程不是主线程,则根据当前线程创建,并以当前线程为keynewLoopvalue存入字典,
  • 3.得到loop后调用_CFSetTSD进行存储
  • 流程图如下:

image.png

  • 从上面分析得知,无论是不是在主线程都需要创建NSRunloopRef

Runloop的数据结构

  • 查看__CFRunLoopCreate函数,可以看到创建和赋值过程: 4.webp -loop的成员中有modes,有item等,他们是什么类型不知道,点进去查看: 5.webp
  • CFRunloop是个结构体,_commonModes_commonModeItems_modes是集合类型,再来看看_currentMode

6.webp

  • 这里我们可以知道CFRunLoopModeRef是个CFRunLoopMode结构体,并且看到了比较熟悉的字眼:sources0sources1oberverstimers,他们是集合或者是数组类型,也就是说Runloop有很多个CFRunLoopMode,每个mode对应多个事件,如下图所示:

7.webp

  • items里面有很多的(source0source1timerobserver),他们是什么不知道,又是怎样依赖modelrunloop里执行?接下来在runloop的底层进行探索分析

runloop底层原理

  • 在使用timer时,通常会加入runloop,我们先从加入runloop开始入手分析,在CFRunLoop.h可以看到有add的几个类型:

8.webp

  • addmode有以下几种类型:
    • commonMode类型CFRunLoopAddCommonMode
    • block类型CFRunLoopPerformBlock
    • Source类型CFRunLoopAddSource
    • Observer类型CFRunLoopAddObserver
    • Timer类型CFRunLoopAddTimer

1. 添加事件

  • CFRunLoopPerformBlock代码如下:

9.webp

  • Runloop`添加事件时主要做了以下几个步骤:
      1. 确保model存在
      1. 创建runloop_block_item单向链表
      1. new_item相关参数进行赋值:
      • 如果尾结点不存在,说明还没有添加mode,则将头结点设置为new_item
      • 如果尾结点存在,则设置尾结点nextnew_item
      • 最后再设置尾结点为new_item
  • CFRunLoopAddCommonMode类型:

10.webp

  • 核心是__CFRunLoopAddItemsToCommonMode

11.webp

  • commonMode类型对应的是sourceobservertimer三种事件,下面将对他们进行一一分析

  • CFRunLoopAddTimer类型:

12.webp

13.webp

  • 添加timer类型的mode比较简单,主要有以下几个步骤:

    • 1.获取相应modeNamemode
    • 2.确保mode中的timers数组存在
    • 3.如果timerrlModes数组中没有新modename,则往rlModes中添加name
    • 4.将timer存入对应modeltimers数组
  • CFRunLoopAddSource类型:

14.webp

  • source类型添加modetimer类型类似:
    • 1.根据modeName获取相应的mode
    • 2.将source赋值给mode中的sources0或者sources1
    • 3.将runloop添加到source中的runLoops
  • CFRunLoopAddObserver类型:

15.webp

  • 添加Observer类型的mode也和上面的类似:
    • 1.根据modeName获取相应的mode
    • 2.在modeobservers数组从后往前遍历:
      • 如果遍历的observer->order小于等于当前observer->order,则将observer插入到下当前index+1
      • 如果遍历完,observer->order都小于数组里的observer->order,则将新的observer插入到数组的第一个位置 从分析几个添加mode的过程,可以感知commonMode中的三个mode过程比较类似,block类型mode有所不同,它是以单向链表的形式添加的。

2. 进入循环

  • 添加完mode后将进行runloopRun,也就是调用函数CFRunLoopRunSpecific,源码如下:

16.webp

  • 1.函数里先根据modeName获取本次运行的mode,如果没有找到则不进入循环

  • 2.将runloopcurrentMode设置成本次运行的mode

  • 3.如果mode类型为entry,则observer通知Runloop即将进入loop

  • 4.调用__CFRunLoopRun函数进行run

  • 5.如果model类型为exit,则observer通知runloop即将退出

  • __CFRunLoopRun函数主要代码如下:

17.webp 主要的是根据状态判断循环条件,然后在循环中处理睡眠及相应事件,核心流程如下:

18.webp

3. 事件处理

  • 1.observers事件: 19.webp
  • 处理observers事件,主要有以下几个步骤:
    • 1.获取observers数组大小
    • 2.根据大小获取或者创建一个CFRunLoopObserverRef类型的collectedObservers
    • 3.遍历mode->observers并对collectedObservers进行相关赋值
    • 4.遍历新的集合并调用__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__执行事件处理
    • 实现如下
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func, CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
   if (func) {
       func(observer, activity, info);
   }
   asm __volatile__(""); // thwart tail-call optimization
}
  • 2.blocks事件: 20.webp
  • blockmoderunloop中是以链表的形式存在,它的执行过程如下:
    • 1.获取链表的头结点和尾结点
    • 2.从头结点往尾结点方向遍历
    • 3.如果满足条件,则执行__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__函数,进而执行block回调
    • 它的核心函数如下
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(void (^block)(void)) {
    if (block) {
        block();
    }
    asm __volatile__(""); // thwart tail-call optimization
}
  • 3.timer事件: 21.webp 执行timer事件先遍历modetimers数组,将满足条件的timer放入新数组,然后遍历执行__CFRunLoopDoTimer函数: 22.webp __CFRunLoopDoTimer函数在满足条件下,执行__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__函数,进而执行回调

  • 4.mainQueue事件 23.webp mainQueue事件是在runloop被唤醒后,然后调用__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__函数,进而调用_dispatch_main_queue_callback_4CF函数处理msg

    1. source0事件:

24.webp

  • source0事件的处理需要判断类型:
    • 如果是ID类型,则在相应的判断后执行__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函数
    • 如果是数组类型,则需要遍历数组,得到的observer在满足条件下执行__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函数
  • 6.source1事件: 25.webpobserver存在,则调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__函数进而执行回调。

一些小的细节

RunLoop休眠的实现原理

image.png 涉及到两个态之间的切换

__CFRunLoopServiceMachPort

  • __CFRunLoopServiceMachPort,该函数是操作 RunLoop 进入休眠的关键,通过设置 mach port 接收消息实现。设置 mach port 接收消息后,线程会暂时挂起进入休眠,等待 mach port 响应。
  • RunLoop 虽然包含了 Source0、Source1、Timers 各种监控对象,但实际上,所有这些监控对象的事件触发(唤醒 RunLoop 去响应事件)都是基于 mach port 实现的:
    • Source0:通过 RunLoop 的_wakeUpPort唤醒 RunLoop;
    • Source1:通过 Source1 绑定的 mach port(Mode 的_portToV1SourceMap)唤醒 RunLoop;
    • Timers:通过 Mode 的_timerPort在恰当的时间点唤醒 RunLoop;
  • 另外,RunLoop 对 GCD 主队列上执行的操作有“特殊照顾”。代码中dispatchPort实现的大致效果是:主线程 RunLoop 在被唤醒并处理完必要的监控对象事件后,会尝试性地从dispatchPort中捞一下是否有需要处理的“GCD调度主线程消息”(这次调用__CFRunLoopServiceMachPort不会使 RunLoop 进入休眠,因为 timeout 参数传了0),如果有则livePort会被置为dispatchPort,并直接跳到handle_msglivePort == dispatchPort逻辑分支中立即处理。这样处理的意图应该是:
    • 保证主线程 RunLoop 的监控对象回调中的“GCD调度主线程操作”可以及时响应;
    • 在主线程已被唤醒时,可以及时响应来自其他子线程的“GCD调度主线程操作”;
  • 总之就是保证“GCD调度主线程操作”的响应优先级。RunLoop 需要进入休眠时,dispatchPort也会被添加到waitSet中并作为__CFRunLoopServiceMachPort入参,这意味着,来自子线程的 dispatch asyn on main 的调用,也是可以唤醒主线程 RunLoop 的。
  • 最后,再留意到代码中的poll变量,poll 这个单词在操作系统领域基本可以理解为轮询方式的多路复用机制。从代码看,当次 Loop 如果是 poll 模式时,不触发kCFRunLoopBeforeWaitingkCFRunLoopAfterWaiting事件,也就是说这次 Loop 线程不会进入休眠。此时__CFRunLoopServiceMachPort的 timeout 参数传0,和处理dispatchPort时是一样的。所以,RunLoop 中处理了 Resource0 的 Loop 是存在特殊性的,处理完 Resource0 后,RunLoop 会尝试性地从 Mode 的所有 mach port 中捞一下是否有需要处理的 mach port 事件,有则立即处理,没有则进入下一次 Loop。
  • __CFRunLoopServiceMachPort的代码。一堆内核编程的代码,十分晦涩。其实只需要关注到其中的调用mach_msg函数的那句代码,注意到入参是MACH_RCV_MSG,说明这里是监听 mach port 接收消息而不是发送消息。
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
    Boolean originalBuffer = true;
    kern_return_t ret = KERN_SUCCESS;
    for (;;) {		/* In that sleep of death what nightmares may come ... */
        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(); }
        // 核心操作:调用mach_msg函数接收消息
        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);

        voucher_mach_msg_revert(*voucherState);
        
        *voucherState = voucher_mach_msg_adopt(msg);
        
        if (voucherCopy) {
            if (*voucherState != VOUCHER_MACH_MSG_STATE_UNCHANGED) {
                *voucherCopy = voucher_copy();
            } else {
                *voucherCopy = NULL;
            }
        }

        CFRUNLOOP_WAKEUP(ret);
        if (MACH_MSG_SUCCESS == ret) {
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            return true;
        }
        if (MACH_RCV_TIMED_OUT == ret) {
            if (!originalBuffer) free(msg);
            *buffer = NULL;
            *livePort = MACH_PORT_NULL;
            return false;
        }
        if (MACH_RCV_TOO_LARGE != ret) break;
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
        if (originalBuffer) *buffer = NULL;
        originalBuffer = false;
        *buffer = realloc(*buffer, buffer_size);
    }
    HALT;
    return false;
}