iOS进阶

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

2019-08-05  本文已影响7人  劳模007_Mars

探索底层原理,积累从点滴做起。大家好,我是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
上一篇下一篇

猜你喜欢

热点阅读