iOS底层-Runloop原理详解

1,485 阅读7分钟

不知道大家有没有这个疑问,创建一个空工程运行起来,即使里面没有其它业务代码,App也不会退出,这是为什么?其实是因为Runloop的关系。那么Runloop到底是什么,又是怎么工作的呢?接下来我们将揭开它神秘的面纱。

案例分析

无处不在的Runloop

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

    截屏2021-11-18 17.48.22.png
    可以这样理解这个图,做一些相应的操作后,Runloop会转变成相应的事件去处理。

  • 代码分析:

    - (void)sourceDemo{
        // 1. __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"天王盖地虎");
        }];
      
        // 2. __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"hello word");
        });
      
        // 3. __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
        void (^block)(void) = ^{
            NSLog(@"123");
        };
      
        block();
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        // 4. __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
        NSLog(@"来了,老弟!!!");
    }
    

    下面拿第一种(timer)案例来打印分析:

    截屏2021-11-19 10.47.25.png

    从打印堆栈可以得出以下结论:

      1. timer事件,在runloop对应的是__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__类型
      1. Foundation中的Runloop,在底层实际对应的是CoreFoundation中的CFRunloop
  • 一共有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__

与其他循环的区别

  • 一般循环的cpu

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

    运行后发现占用30%左右的cpu

    截屏2021-11-19 10.00.09.png

  • Runloop所占cpu

    截屏2021-11-19 10.30.47.png 运行一个空的工程,发现cpu始终为0,但工程一直在运行

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

Runloop到底是不是死循环,是怎样的一个循环呢?接下来我们走进源码的世界

源码解读

  • 在上面的堆栈分析,我们可以得出如下的关系:

    截屏2021-11-19 13.37.25.png

    • Runloop在底层是CFRunloop,解析来我们将对CFRunloop进行分析
  • Source Browser中下载 CoreFoundation源码 ,然后对CFRunloop.c对`文件进行阅读

创建

  • 在源码中有一个CFRunLoopRun方法:

    void CFRunLoopRun(void) {    /* DOES CALLOUT */
        int32_t result;
        do {
            result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
            CHECK_FOR_FORK();
        } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
    }
    
    • 主要是通过判断result的结果进行do-while循环,说明这里是runloop创建的过程
  • 先来看看CFRunLoopGetCurrent,它是获得CFRunLoopRef的函数:

    CFRunLoopRef CFRunLoopGetCurrent(void) {
        CHECK_FOR_FORK();
      
        CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
        if (rl) return rl;
      
        return _CFRunLoopGet0(pthread_self());
    }
    
    • 这里先根据类型从TSD获取rl,如果存在则返回,如果不存在则根据当前线程从_CFRunLoopGet0函数获取
  • _CFRunLoopGet0

    截屏2021-11-19 15.06.05.png

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

    截屏2021-11-19 16.30.09.png

  • 从上面分析得知,无论是不是在主线程都需要创建NSRunloopRef,那么这个runloop是个怎样的数据结构,接下来我们继续分析。

数据结构

  • 进行__CFRunLoopCreate函数,可以看到创建和赋值过程:

    截屏2021-11-19 16.38.17.png

    • loop的成员中有model,有item等,他们是什么类型不知道,点进去查看:

      截屏2021-11-19 17.41.35.png

    • 这个我们知道CFRunloop是个结构体,_commonModes_commonModeItems_modes是集合类型,再来看看_currentMode

    截屏2021-11-19 17.58.40.png

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

      截屏2021-11-19 18.26.30.png

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

底层原理

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

    截屏2021-11-21 22.43.16.png

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

1. 添加事件

  • CFRunLoopPerformBlock代码如下:

    截屏2021-11-21 23.18.43.png

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

    截屏2021-11-21 23.36.47.png

    • 此处函数的核心是__CFRunLoopAddItemsToCommonMode,它的代码如下:

      截屏2021-11-21 23.50.32.png
      原来commonMode类型对应的是sourceobservertimer三种事件,下面将对他们进行一一分析

  • CFRunLoopAddTimer类型:

    截屏2021-11-22 10.39.07.png

    截屏2021-11-22 11.08.15.png

    • 添加timer类型的mode比较简单,主要有以下几个步骤:
        1. 获取相应modeNamemode
        1. 确保mode中的timers数组存在
        1. 如果timerrlModes数组中没有新modename,则往rlModes中添加name
        1. timer存入对应modeltimers数组
  • CFRunLoopAddSource类型:

    截屏2021-11-22 11.44.17.png

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

    截屏2021-11-22 13.57.52.png

    • 添加Observer类型的mode也和上面的类似:
        1. 根据modeName获取相应的mode
        1. modeobservers数组从后往前遍历:
        • 如果遍历的observer->order小于等于当前observer->order,则将observer插入到下当前index+1
        • 如果遍历完,observer->order都小于数组里的observer->order,则将新的observer插入到数组的第一个位置

从分析几个添加mode的过程,可以感知commonMode中的三个mode过程比较类似,block类型mode有所不同,它是以单向链表的形式添加的。

2. 进入循环

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

    截屏2021-11-22 14.31.21.png

      1. 函数里先根据modeName获取本次运行的mode,如果没有找到则不进入循环
      1. runloopcurrentMode设置成本次运行的mode
      1. 如果mode类型为entry,则observer通知Runloop即将进入loop
      1. 调用__CFRunLoopRun函数进行run
      1. 如果model类型为exit,则observer通知runloop即将退出
  • __CFRunLoopRun函数主要代码如下:

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

    截屏2021-11-22 17.46.47.png

3. 事件处理

    1. observers事件:

    截屏2021-11-23 09.15.15.png

    • 处理observers事件,主要有以下几个步骤:
        1. 获取observers数组大小
        1. 根据大小获取或者创建一个CFRunLoopObserverRef类型的collectedObservers
        1. 遍历mode->observers并对collectedObservers进行相关赋值
        1. 遍历新的集合并调用__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
        }
        
    1. blocks事件:

    截屏2021-11-23 09.52.37.png

    • blockmoderunloop中是以链表的形式存在,它的执行过程如下:
        1. 获取链表的头结点和尾结点
        1. 从头结点往尾结点方向遍历
        1. 如果满足条件,则执行__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
        }
        
    1. timer事件:

    截屏2021-11-23 10.12.02.png
    执行timer事件先遍历modetimers数组,将满足条件的timer放入新数组,然后遍历执行__CFRunLoopDoTimer函数:

    截屏2021-11-23 10.48.02.png
    __CFRunLoopDoTimer函数在满足条件下,执行__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__函数,进而执行回调

    1. mainQueue事件

    截屏2021-11-23 10.58.27.png
    mainQueue事件是在runloop被唤醒后,然后调用__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__函数,进而调用_dispatch_main_queue_callback_4CF函数处理msg

    1. source0事件:

    截屏2021-11-23 11.03.02.png
    source0事件的处理需要判断类型:

    • 如果是ID类型,则在相应的判断后执行__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函数
    • 如果是数组类型,则需要遍历数组,得到的observer在满足条件下执行__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函数
    1. source1事件: source1的类型要简单的多:

    截屏2021-11-23 11.06.54.png
    observer存在,则调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__函数进而执行回调。