美文网首页iOS进阶
iOS底层原理探索—RunLoop的本质

iOS底层原理探索—RunLoop的本质

作者: 劳模007_Mars | 来源:发表于2019-08-05 23:52 被阅读7次

    探索底层原理,积累从点滴做起。大家好,我是Mars。

    往期回顾

    iOS底层原理探索—OC对象的本质
    iOS底层原理探索—class的本质
    iOS底层原理探索—KVO的本质
    iOS底层原理探索— KVC的本质
    iOS底层原理探索— Category的本质(一)
    iOS底层原理探索— Category的本质(二)
    iOS底层原理探索— 关联对象的本质
    iOS底层原理探索— block的本质(一)
    iOS底层原理探索— block的本质(二)
    iOS底层原理探索— Runtime之isa的本质
    iOS底层原理探索— Runtime之class的本质
    iOS底层原理探索— Runtime之消息机制

    今天带领大家继续探索RunLoop的本质

    RunLoop

    顾名思义,运行循环,在程序运行过程中循环做一些事情。如果没有RunLoop,程序执行完毕就会立即退出,如果有RunLoop程序并不会马上退出,而是保持运行状态,等待处理程序的各种事件。
    RunLoop可以保持程序的持续运行,在没有事件处理的时候使程序进入休眠模式,从而节省CPU资源,提高程序性能。

    没有RunLoop.png
    我们可以看到,程序在打印完Hello,World!后输出一句Program ended with exit code: 0,说明程序结束了,这是没有RunLoop的情况。
    有RunLoop.png
    RunLoop,程序就不会马上退出,而是保持运行状态。
    苹果官方文档中用一张示意图为我们展示了RunLoop的运行逻辑:
    RunLoop运行逻辑.jpg
    从图中可以看出,RunLoop在运行循环过程中,接收到Input sources或者Timer sources时会通过对应处理方式处理;没有事件消息传入的时候,就会使程序处于休眠状态。

    我们进入上面有RunLoop的代码中的UIApplicationMain内部查看,发现返回值是int类型,那么我们对上面的main函数做一些改动:

    main函数.png
    通过运行程序发现只打印开始了,并不会打印结束了。我们知道在程序运行时会开启一条主线程,通过测试说明在UIApplicationMain函数中,开启了一个和主线程相关的RunLoop,导致UIApplicationMain不会返回,一直在运行中,也就保证了程序的持续运行。
    RunLoop在底层中有部分是开源的,我们可以进入官方文档下载源码帮助我们分析。
    下面来看RunLoop的源码:
    RunLoop源码.png
    我们发现RunLoop底层其实是一个do while循环,通过判断result的值来实现。因此,我们可以把RunLoop看成一个死循环。如果没有RunLoopUIApplicationMain函数执行完毕之后将直接返回,也就没有程序持续运行一说了。

    RunLoop基本作用

    1、保持程序持续运行:程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行。
    2、处理App中的各种事件:比如触摸事件、定时器事件、Selector事件等。
    3、节省CPU资源,提高程序性能:程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CUP,现在没有事情做,我要去休息,这时CUP就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情。

    RunLoop对象

    iOS中提供了两套API来访问和使用RunLoopFundation框架中的NSRunLoop对象、CoreFoundation中的CFRunLoopRef对象
    我们知道,CoreFoundation是一套C语言API,而Fundation框架则是基于CoreFoundationOC语言封装的。CFRunLoopRef的源码是开源的,我们可以从官方文档下载源码,接下来的分析也是基于CFRunLoopRef对象进行分析。

    获取RunLoop对象

    //Foundation
    [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
    [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
    
    //Core Foundation
    CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
    CFRunLoopGetMain(); // 获得主线程的RunLoop对象
    

    RunLoop与线程的关系

    上文通过CFRunLoopGetCurrent();函数获得当前线程的RunLoop对象,我们进入源码查看:

    // 获得当前线程的RunLoop对象,内部调用_CFRunLoopGet0函数
    CFRunLoopRef CFRunLoopGetCurrent(void) {
        CHECK_FOR_FORK();
        CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
        if (rl) return rl;
        return _CFRunLoopGet0(pthread_self());
    }
    
    // 查看_CFRunLoopGet0方法
    CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
        if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
        }
        __CFLock(&loopsLock);
        if (!__CFRunLoops) {
            __CFUnlock(&loopsLock);
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        // 根据传入的主线程获取主线程对应的RunLoop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        // 保存主线程 将主线程-key和RunLoop-Value保存到字典中
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        CFRelease(mainLoop);
            __CFLock(&loopsLock);
        }
        
        // 从字典里面拿,将线程作为key从字典里获取一个loop
        CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        __CFUnlock(&loopsLock);
        
        // 如果loop为空,则创建一个新的loop,所以runloop会在第一次获取的时候创建
        if (!loop) {  
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
            __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        
        // 创建好之后,以线程为key runloop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runloop
        if (!loop) { 
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
            // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
            __CFUnlock(&loopsLock);
        //线程结束是销毁loop
        CFRelease(newLoop);
        }
        if (pthread_equal(t, pthread_self())) {
            _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
            if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
                _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
            }
        }
        return loop;
    }
    

    通过源码分析可以看出,线程和RunLoop之间是一一对应的,其关系是保存在一个Dictionary字典里。所以我们创建子线程RunLoop时,只需在子线程中获取当前线程的RunLoop对象即可[NSRunLoop currentRunLoop];。如果不获取,那子线程就不会创建与之相关联的RunLoop,并且只能在一个线程的内部获取其RunLoop

    当通过调用[NSRunLoop currentRunLoop];方法获取RunLoop时,会先看一下字典里有没有子线程对应的RunLoop,如果有则直接返回RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。当线程结束时,RunLoop会被销毁。

    分析至此我们可以得出总结:

    1、每条线程都有唯一的一个与之对应的RunLoop对象
    2、RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
    3、主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
    4、RunLoop在第一次获取时创建,在线程结束时销毁

    RunLoop底层结构

    在源码中找到__CFRunLoop类型的结构体:

    CFRunLoop底层结构.png
    我们重点分析CFRunLoopModeRef _currentMode;CFMutableSetRef _modes;这两个成员变量:

    CFMutableSetRef _modes;是一个集合,里面包含一个或者多个model

    CFRunLoopModeRef _currentMode;是当前的model,代表RunLoop的运行模式,指向__CFRunLoopMode结构体的指针。

    我们查看一下__CFRunLoopMode结构体源码:

    __CFRunLoopMode结构体.png
    红色标注的代码,有两个集合Set,两个数组Array,里面分别保存着Source0Source1TimersObserver,它们分别代表什么呢?

    Source0: 触摸事件处理、performSelector:onThread:

    Source1: 基于Port的线程间通信、系统事件捕捉

    Timers:NSTimer、performSelector:withObject:afterDelay:

    Observers: 用于监听RunLoop的状态、UI刷新(BeforeWaiting)、Autorelease pool(BeforeWaiting)

    通过上面分析我们知道,CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Mode——CFRunLoopMode,每个Mode又包含若干个Source0Source1TimerObserver,而RunLoop启动时只能选择其中一个Mode作为currentMode

    通过上面的分析,我们对RunLoop内部结构有了大致的了解,接下来分析RunLoop中的相关类都有哪些及作用。

    RunLoop相关类及作用

    Core Foundation中提供了关于RunLoop的5个类:

    1、CFRunLoopRef:获得当前RunLoop和主RunLoop

    2、CFRunLoopModeRef:运行模式

    3、CFRunLoopSourceRef:事件源,输入源

    4、CFRunLoopTimerRef:定时器时间

    5、CFRunLoopObserverRef:观察者

    1. CFRunLoopModeRef

    CFRunLoopModeRef代表RunLoop的运行模式。

    一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0Source1TimerObserver

    示意图.png
    每次RunLoop启动时,只能指定其中一个Mode,这个Mode被称作 CurrentMode。如果需要切换Mode,只能退出RunLoop,再重新指定一个Mode进入,这样做主要是为了分隔开不同组的Source0Source1TimerObserver,让其互不影响。如果Mode里没有任何Source0Source1TimerObserverRunLoop会立马退出

    注意:一种Mode中可以有多个Source0Source1TimerObserver。但是必须至少有一个Source或者Timer,因为如果Mode为空,RunLoop运行到空模式不会进行空转,就会立刻退出。

    系统默认注册的5个Mode:

    RunLoop有五种运行模式:

    1. kCFRunLoopDefaultModeApp的默认Mode,通常主线程是在这个Mode下运行

    2.UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响

    3.UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode

    4.GSEventReceiveRunLoopMode: 接受系统事件的内部Mode,通常用不到

    5.kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultModeUITrackingRunLoopMode用,并不是一种真正的Mode

    2、CFRunLoopSourceRef事件源(输入源)

    Source分为Source0Source1两种

    Source0:非基于Port的,用于用户主动触发的事件(点击button 或点击屏幕)

    Source1:基于Port的,通过内核和其他线程相互发送消息(与内核相关)

    3、CFRunLoopObserverRef

    CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变,包括唤醒,休息,以及处理各种事件。
    系统为我们提供了一下几种状态:

    RunLoop Observer状态.png

    RunLoop的运行逻辑

    在文章开头我们简要概括了一下RunLoop的运行逻辑,下面通过源码分析来详细阐述一下(源码已经做过简化,只介绍主要流程):

    // 公开的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);
    }
    
    // 经过精简的 CFRunLoopRunSpecific 函数代码,其内部会调用__CFRunLoopRun函数
    SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    
        // 通知Observers : 进入Loop
        // __CFRunLoopDoObservers内部会调用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__函数
        if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
        
        // 核心的Loop逻辑
        result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
        
        // 通知Observers : 退出Loop
        if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
        return result;
    }
    
    // 精简后的 __CFRunLoopRun函数,保留了主要代码
    static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
        int32_t retVal = 0;
        do {
            // 通知Observers:即将处理Timers
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); 
            
            // 通知Observers:即将处理Sources
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
            
            // 处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
            
            // 处理Sources0
            if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
                // 处理Blocks
                __CFRunLoopDoBlocks(rl, rlm);
            }
            
            //判断有没有Sources1
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                // 如果有Sources1,就跳转到handle_msg标记处
                goto handle_msg;
            }
            
            // 通知Observers:即将休眠
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
            
            // 进入休眠,等待其他消息唤醒
            __CFRunLoopSetSleeping(rl);
            __CFPortSetInsert(dispatchPort, waitSet);
            do {
                __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
            } while (1);
            
            // 醒来
            __CFPortSetRemove(dispatchPort, waitSet);
            __CFRunLoopUnsetSleeping(rl);
            
            // 通知Observers:已经唤醒
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
            
        handle_msg: // 看看是谁唤醒了RunLoop,进行相应的处理
            if (被Timer唤醒的) {
                // 处理Timer
                __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
            }
            else if (被GCD唤醒的) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } else { // 被Sources1唤醒的
                __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
            }
            
            // 执行Blocks
            __CFRunLoopDoBlocks(rl, rlm);
            
            // 根据之前的执行结果,来决定怎么做,为retVal赋相应的值
            if (sourceHandledThisLoop && stopAfterHandle) {
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout_context->termTSR < mach_absolute_time()) {
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(rl)) {
                __CFRunLoopUnsetStopped(rl);
                retVal = kCFRunLoopRunStopped;
            } else if (rlm->_stopped) {
                rlm->_stopped = false;
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
                retVal = kCFRunLoopRunFinished;
            }
            
        } while (0 == retVal);
        
        return retVal;
    }
    

    我们用一张流程图将RunLoop运行逻辑源码梳理一下:


    RunLoop运行逻辑.png

    RunLoop退出

    RunLoop在一下三种情况下回退出:

    1、主线程销毁,RunLoop退出;
    2、Mode中有一些TimerSourceObserver,这些保证Mode不为空时,RunLoop没有空转并且是在运行的。当Mode中为空的时候,RunLoop会立刻退出;
    3、启动RunLoop的时候可以设置什么时候停止;

    手动设置RunLoop的停止时间:

    [NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
    [NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>
    

    对于RunLoop的本质分析到此就结束了,下篇我们继续分析RunLoop的应用。

    更多技术知识请关注公众号
    iOS进阶


    iOS进阶.jpg

    相关文章

      网友评论

        本文标题:iOS底层原理探索—RunLoop的本质

        本文链接:https://www.haomeiwen.com/subject/dhvmdctx.html