iOS RunLoop底层原理分析

准备工作

  • coreFoundation
  • swift-corelibs-foundation

1. 什么是RunLoop

RunLoop是一个运行循环,也是一个对象,并且提供了入口函数,进行do while循环,保证运行程序不退出

一个程序运行结束的标志性语句是return,在iOS应用的入口main函数中,return并执行了一个UIApplicationMain函数,如下:

main函数

既然已经return了,为什么应用依然可以接收消息,处理消息呢?程序不应该到此结束吗?我们在代理AppDelegateapplication:didFinishLaunchingWithOptions:方法中添加断点,并bt打印堆栈信息,探索到以下内容,如下:
查看堆栈信息

由堆栈信息看出,程序的执行流程,首先dyld进行应用程序加载,执行main函数,启动RunLoop,加载GCD………由此可见应用程序在启动过程中进行了一系列的初始化工作。同时可以确定,RunLoop来自CoreFoundation框架,CoreFoundation的部分源码是开源的,其中包括RunLoop。在其源码中,RunLoop Run的实现也确实是一个do while循环。如下:

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

我们知道RunLoop线程有关系,并且提供了一套消息处理机制。在苹果官方的开发者文档Documentation Archive中搜索Thread内容,其中就包含了RunLoop的相关说明,如下:

官方文档说明

这也说明了Runloop线程是息息相关的,这也是我们以下需要分析的。

2. RunLoop的作用

RunLoop的作用总结如下几点:

  • 保持程序的持续运行
  • 处理APP中的各种事件(触摸定时器performSelector
  • 节省cpu资源、提高程序的性能。

2.1 保持程序的持续运行

main函数创建UIApplicationMain时启动了RunLoop,如果没有启动RunLoop,程序就会直接退出

2.2 处理APP中的各种事件

在苹果的官方文档中,有这样一张图:


结合官方的说明,我们可以知道,RunLoop是线程用于处理运行事件处理响应事件循环。这些事件包括port事件源、屏幕触摸事件、performSelectortimer等。

我们在写上层代码的时候很少接触到Runloop,因为Runloop封装的非常好。一些事件处理都运用到了RunLoop,下面通过案例来分析:

  • Timer
    处理Timer事件,对应__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__,如下:
    案例分析
  • performSelector
    处理performSelector事件,也对应的__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__,如下:
    案例分析
  • GCD
    队列中处理事件,对应__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__,如下:
    案例分析

    通过上面的探索,我们可以发现这些事件的处理方法均以__CFRUNLOOP_IS_为开头的方式命名,查看源码,其会根据不同的事件,提供不同的响应方法,如下:
    响应方法

    source相关响应方法

事件处理回调方法总结:

  • block应用:__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
  • 调用timer__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
  • 响应source0__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
  • 响应source1__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
  • GCD主队列:__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
  • observer源:__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

2.3 节省cpu资源、提高程序的性能

RunLoop能节省cpu资源,提高程序的性能,这点体现在哪呢?见如下案例:

案例分析

从以上的案例可以发现,应用程序启动后,保持运行状态,但是此时的cpu占用率一直是0%。我们知道,RunLoop实际上就是一个do while循环,我们如果开启一个循环会怎样呢?对比一下!
开启一个循环

通过上图可以发现,cpu占用一直很高,所以可以得出结论,RunLoop所提供的循环是和普通的循环是有区别的,有事需要处理才会运行没有事则会休息!从而达到了节省cpu资源提高程序性能的作用。那么这种功能是如何实现的,下面会分析。

3. RunLoop与线程的关系

RunLoop线程是息息相关的,并且是一一对应的关系。那么他们的关系是如何建立的呢?

    // 获取main RunLoop
    NSLog(@"%@", CFRunLoopGetMain());
    // 获取当前 RunLoop
    NSLog(@"%@", CFRunLoopGetCurrent());

通常会通过上面的两种方式打印输出main RunLoop以及当前RunLoop。在CFRunLoop源码中查看其实现,如下:

//获取主线程的Runloop
CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}
//获取当前线程的Runloop
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

根据上面的代码不难发现,获取RunLoop均是通过线程进行获取。那么线程和RunLoop的关系是如何建立的呢?这就需要解读_CFRunLoopGet0函数的实现源码,如下:

_CFRunLoopGet0 - 1

_CFRunLoopGet0 - 2

解读_CFRunLoopGet0源码:

  • 维护了一个CFMutableDistionaryRef字典__CFRunLoops,字典默认为NULL
  • 如果CFRunLoops是空,则创建一个CFMutableDistionaryRef字典,并默认初始化主线程的RunLoop
  • 将创建的RunLoop放入到CFMutableDistionaryRef字典中,也就是放入__CFRunLoops中,以线程为keyRunLoopvalue
  • 在通过线程获取RunLoop时,以key-value方式从字典中获取对应的RunLoop
  • 如果RunLoop为空,则创建一个newLoop,以线程为keyRunLoop为value,存储到__CFRunLoops中;
  • 返回线程对应的RunLoop

结论:主线程的RunLoop会默认被创建,而子线程的RunLoop懒加载的,需要时才会创建RunLoop线程一对一的关系,存储在一个字典中。

  • 子线程RunLoop案例分析
    GFThread * gfThread = [[GFThread alloc] initWithBlock:^{
        NSLog(@"running....");

        [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"helloc timer...%@", [NSThread currentThread]);
        }];
    }];

    gfThread.name = @"Hello.thread";
    [gfThread start];

GFThread是一个继承自NSThread的自定义线程,并重写了dealloc方法。案例中,在子线程中使用了一个NSTimer,运行这段代码会是什么结果呢?请往下看:

运行结果

线程生命周期结束,但是NSTimer的任务并没有执行这是因为NSTimer需要依赖于RunLoop,主线程的RunLoop默认开启,而子线程的RunLoop懒加载,需要手动开启

对上面的案例进行修改,启动子线程的RunLoop。如下:

案例分析

那么如何结束NSTimer呢?首先我们需要理理清楚一个关系:线程和RunLoop一一对应,而NSTimer又依赖于RunLoop。根据这个思路可以做如下修改:
案例分析

通过外部变量可以控制线程,如果线程退出,对应的RunLoop也会停止运行,NSTimer又依赖于RunLoop,也自然不能运行。

4. RunLoop数据结构

RunLoop中涉及到5个重要的类,分别如下:

  • CFRunLoop - RunLoop对象
  • CFRunLoopMode - 五种运行模式
  • CFRunLoopSource - 输入源/事件源,包括Source0Source1
  • CFRunLoopTimer - 定时源,也就是NSTimer
  • CFRunLoopObserver - 观察者,用来监听RunLoop

1. CFRunLoop
在底层RunLoop对象为CFRunLoopNSRunLoopOC层的封装。我们可以通过以下两种方式获取当前线程的RunLoop:

    // c/c++
    CFRunLoopRef lp     = CFRunLoopGetCurrent();
    // OC
    NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];

CFRunLoop在底层是如何定义的呢?查找得出源码如下:

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

CRRunLoop中包括锁_lock、用于处理source1的唤醒端口_wakeUpport、关联的线程_pthread、当前模式_currentMode等。同时维护了一个set集合_modes。通过其结构体的数据,我们可以得出结论:RunLoopmode是一对多的关系。同时包括_commonModes属性,commonMode是一个伪模式

2. CFRunLoopMode
可以通过以下代码获取当前线程RunLoopcurrentModemode列表:

    CFRunLoopRef lp     = CFRunLoopGetCurrent();
    CFRunLoopMode mode  = CFRunLoopCopyCurrentMode(lp);
    NSLog(@"mode == %@",mode);

    CFArrayRef modeArray= CFRunLoopCopyAllModes(lp);
    NSLog(@"modeArray == %@",modeArray);

运行上面的代码,查看运行结果,如下:

运行结果

此时currentModekCFRunLoopDefaultMode,而当前线程的RunLoop包括了三种mode,分别是:UITrackingRunLoopModeGSEventReceiveRunLoopModekCFRunLoopDefaultMode

3. 案例了解mode的切换
引入下面的案例,用于了解mode的切换过程,如下:

model切换案例

RunLoop添加Timer时放在了DefaultMode,程序正常情况下也运行在DefaultMode,但在滚动视图时,切换到了UITrackingModetimer事件也不再触发。当停止滚动视图,又切回到了DefaultModetimer恢复运行

为什么model切换timer就停止运行?需要什么操作才能解决这样子的问题?请继续往下走。

首先查看model的定义如下:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

__CFRunLoopMode源码定义中包括了4set集合_sources0_sources1_observers_timers,这四个集合也就是我们常说的事件事务)。所以我们可以得出结论:CFRunLoopModesoursestimerobserver也是一对多的关系。

Developer Document中搜索NSRunLoopMode可以找到,系统共维护了5mode见下图:

Developer Document

  • kCFRunLoopDefaultMode 默认的运行模式,通常主线程是在这个Mode下运行
  • UITrackingRunLoopMode 界面跟踪Mode,用于ScrollView等视图,追踪触摸滑动,保证界面的滑动不受其他Mode的影响
  • UIInitializationRunLoopMode 在刚启动App时进入的第一个Mode,启动完成后就不再使用
  • GSEventReceiveRunLoopMode 接受系统时间的内部Mode,通常用不到
  • kCFRunLoopCommonModes 是一个伪模式,可以在标记为CommonModes的模式下运行,RunLoop会自动将_commonModeItems里的sourceobservetimer同步到具有标记的Mode里。

综上,可以得出以下关系图:


关系图
  • RunLoop与线程一对一
  • RunLoopMode一对多
  • Modesourcetimerobserver也是一对多

5. RunLoop事件处理机制

RunLoop处理APP中的各种事件(触摸、定时器、performSelector),也就是说blocktimersource0source1GCDobserver都需要依赖于RunLoop,那么这些事件是如何加入到RunLoop中的呢?底层又是如何去处理这些事件的呢?让我们继续往下探讨吧!!

5.1 添加事务

在源码中提供了一些事务(事件)添加方法,这些事务会添加到对应的mode中,如下:

事务添加

5.1.1 block事务添加

当有block事务时,RunLoop会调用CFRunLoopPerformBlock方法,将block事务存储到对应的mode中,如下:

CFRunLoopPerformBlock

在此过程中首先进行mode的判断处理,确定需要将事务放到哪个mode中,如果mode或者block为空,则释放;否则会创建一个block_item,该数据是一个链表结构,其存储了一个block下一个节点地址信息

5.1.2 timer事务添加

当有timer事务时,RunLoop会调用CFRunLoopAddTimer方法,将timer事务存储到对应的mode中,如下:

CFRunLoopAddTimer

进入__CFRepositionTimerInMode方法,如下:
__CFRepositionTimerInMode

这里会对mode进行判断,判断是否为commonModes,如果是会初始化_commonModeItems集合,并将timer事务添加到集合中。否则找到对应的mode,然后调用__CFRepositionTimerInMode方法,将timer添加到_timers集合中。

注意:CFRunLoopAddObserverCFRunLoopAddSource流程类似,这里不详细说明。

5.2 RunLoop循环

在程序运行的入口处设置断点,我们可以发现系统会首先调用CFRunLoopRunSpecific方法,启动RunLoop。如下:

汇编断点

下面跟踪RunLoop处理流程。在源码中查找CFRunLoopRunSpecific的方法实现,如下:
CFRunLoopRunSpecific

在这里注册了两个Observer,第一个Observer监视的事件是Entry(即将进入Loop),第二个Observer监视Exit(即将退出Loop)。

进入__CFRunLoopRun方法,方法实现如下:

__CFRunLoopRun - 1

__CFRunLoopRun - 2

__CFRunLoopRun - 3

__CFRunLoopRun - 4

__CFRunLoopRun - 5

__CFRunLoopRun - 6

__CFRunLoopRun - 7

__CFRunLoopRun - 8

__CFRunLoopRun - 9

循环状态是对retVal进行控制,在循环过程中,会对状态进行判断,如,是否TimedOut、是否Stopped、是否Finished,来确定RunLoop是否需要销毁
此部分代码是RunLoop核心流程,关键点做了标注,总结下来,可以得出下面这个流程图:
流程图

5.3 事务处理

上面两节已经摸清楚事务(事件)的添加流程RunLoop循环处理流程,下面重点分析RunLoop在循环过程中是如何处理事务的。
在上面的do while循环中,有处理事务的入口:__CFRunLoopDoBlocks__CFRunLoopDoTimers__CFRunLoopDoSources0__CFRunLoopDoSource1

5.3.1 处理block事务

__CFRunLoopDoBlocks源码实现,如下:

__CFRunLoopDoBlocks - 1

__CFRunLoopDoBlocks - 2

在分析block事务添加过程时提到,block事务是以链表的形式存储的,这里进行处理事务时通过_next指针循环遍历所有的block事务。

block执行逻辑:

  • 事务加入的mode和当前RunLoopmode相等
  • 当前modecommonModes
  • 通过调用回调函数__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__执行任务
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(void (^block)(void)) {
    if (block) {
        block();
    }
    asm __volatile__(""); // thwart tail-call optimization
}
5.3.2 处理timer事务

__CFRunLoopDoTimers源码实现如下:

__CFRunLoopDoTimers

此过程中,会从当前mode_timers中获取需要执行的timer事务,放入到数组timers中,然后在调用__CFRunLoopDoTimer方法执行timer__CFRunLoopDoTimer实现原理如下:
__CFRunLoopDoTimer

在此流程中会对Timer的状态进行判断,并调用函数__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__完成事务的执行。

__CFRunLoopDoSources0的处理流程不再详细介绍,最终会调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函数。

5.4 总结流程图

总结流程图

6. RunLoop与AutoreleasePool

AutoreleasePool创建和释放

  • App启动后,苹果在主线程RunLoop里注册了两个Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()
  • 第一个Observer监视的事件是Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush()创建自动释放池。其order-2147483647优先级最高保证创建释放池发生在其他所有回调之前
    -第二个Observer监视了两个事件:BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()释放旧的池并创建新池Exit(即将退出Loop) 时调用_objc_autoreleasePoolPop()来释放自动释放池。这个Observerorder2147483647优先级最低保证其释放池子发生在其他所有回调之后
  • 在主线程执行的代码,通常是写在诸如事件回调Timer回调内的。这些回调会被RunLoop创建好的AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建Pool了。

总结:

  • AutoreleasePool创建是在一个RunLoop事件开始之前(push)
  • AutoreleasePool释放是在一个RunLoop事件即将结束之前(pop)
  • AutoreleasePool里的Autorelease对象的加入是在RunLoop事件中,AutoreleasePool里的Autorelease对象的释放是在AutoreleasePool释放时。

7. RunLoop总结

RunLoop是通过系统内部维护的循环进行事件、消息管理的一个对象RunLoop实际上就是一个do...while循环,有任务时开始,无任务时休眠。本质是通过mach_msg()函数接收、发送消息

  • RunLoop线程的关系:

    • RunLoop的作用就是来管理线程,当线程的RunLoop开启后,线程就会在执行完任务后,处于休眠状态,随时等待接受新的任务,不会退出
    • 只有主线程的RunLoop是默认开启的,其他线程的RunLoop需要手动开启。所以当程序开启后,主线程一直运行,不会退出。
  • RunLoop中涉及到5个重要的类:

    • CFRunLoop - RunLoop对象
    • CFRunLoopMode - 五种运行模式
    • CFRunLoopSource - 输入源/事件源,包括Source0Source1
    • CFRunLoopTimer - 定时源,也就是NSTimer
    • CFRunLoopObserve - 观察者,用来监听RunLoop
  • CFRunLoopMode - 五种运行模式

    • kCFRunLoopDefaultMode 默认的运行模式,通常主线程是在这个Mode下运行
    • UITrackingRunLoopMode 界面跟踪Mode,用于ScrollView等视图,追踪触摸滑动,保证界面的滑动不受其他Mode的影响
    • UIInitializationRunLoopMode 在刚启动App时进入的第一个Mode,启动完成后就不再使用
    • GSEventReceiveRunLoopMode 接受系统时间的内部Mode,通常用不到
    • kCFRunLoopCommonModes 是一个伪模式,可以在标记为CommonModes的模式下运行,RunLoop会自动将_commonModeItems里的sourceobservetimer同步到具有标记的Mode里。
  • CFRunLoopSource - 事件源

    • Source1:基于mach_port回调函数指针,也就是端口通讯,处理来自系统内核或其他进程的事件,比如点击手机屏幕
    • Source0:非基于Port的处理事件,也就是应用层事件(内部事件、APP负责管理的事件,UIEvent),包含一个回调函数指针,需要手动标记为待处理或者手动唤醒RunLoop,如performSelectorblock
    • 例如:一个APP在前台静止,用户点击APP界面,屏幕表面的时事件会先包装成Event告诉source1(基于mach_port),source1唤醒RunLoop将事件Event分发给source0,由source0来处理。
  • CFRunLooTimer - 定时源
    就是NSTimer,在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的Timer是不准确的,因为RunLoop只负责分发源消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延时,或者少执行一次)。

  • CFRunLoopObserver - 观察者
    用来监听时间点事件CFRunLoopActivity

    • KCFRunLoopEntery RunLoop准备启动
    • kCFRunLoopBeforeTimers RunLoop将要处理一些Timer相关的事件
    • kCFRunLoopBeforeSources RunLoop将要处理一些Source事件
    • kCFRunLoopBeforeWaiting RunLoop将要进行休眠状态,即将由用户状态切换内核态
    • kCFRunLoopAfterWaiting RunLoop被唤醒,即从内核态切换到用户态
    • kCFRunLoopExit RunLoop退出
    • kCfRunLoopAllActivitires 监听所有状态
  • 各数据结构之间的联系

    • RunLoop和线程是一对一的关系
    • RunLoopRunLoopMode是一对多的关系
    • RunLoopModeRunLoopSource是一对多的关系
    • RunLoopModeRunLoopTimer是一对多的关系
    • RunLoopModeRunLoopObserver是一对多的关系
  • 为什么main函数能够保持一直存在且不退出?
    main函数内容会调用UIApplication函数,而在UIAPPlicationMain内部会启动主线程的RunLoop,可以做到有消息处理,能够迅速从内核态到用户态的切换,立刻唤醒处理,而没有消息处理时,通过用户态到内核态的切换进入等待状态,避免资源的占用。因此main函数能够一直存在并且不退出。

你可能感兴趣的:(iOS RunLoop底层原理分析)