RunLoop(一):源码与逻辑

作者 : 开心源码 本文共7092个字,预计阅读时间需要18分钟 发布时间: 2022-05-12 共213人阅读

简述

什么是RunLoop?顾名思义RunLoop是一个运行循环,它的作用是使得程序在运行之后不会马上退出,保持运行状态,来解决少量触摸事件、定时器时间等。RunLoop可以使得线程在有任务的时候解决任务,没有任务的时候休眠,以此来节省CPU资源,提高程序性能。

那RunLoop是怎么保持程序的运行状态,究竟解决了哪些事件?下面我们就从源码的层面来理解一下RunLoop。

RunLoop

获取runloop对象

NSRunLoop和CFRunLoopRef都代表RunLoop对象,NSRunLoop是对CFRunLoopRef的封装。

Foundation

[NSRunLoop currentRunLoop]; // 取得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 取得主线程的RunLoop对象

Core Foundation

CFRunLoopGetCurrent(); // 取得当前线程的RunLoop对象
CFRunLoopGetMain(); // 取得主线程的RunLoop对象

RunLoop相关类

从源码的代码结构中我们可以找出来一下5个跟RunLoop相关的结构

CFRunLoopRefCFRunLoopModeRefCFRunLoopSourceRefCFRunLoopObserverRefCFRunLoopTimerRef

下面是CFRunLoopRef的结构代码

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;};

变量很多,我们不需要一律看,只要要注意这两个

CFRunLoopModeRef _currentMode;CFMutableSetRef _modes;

每一个runloop里面有很多mode(存在一个set集合里面),而后之后后一个mode叫做currentMode,也就是说runloop一次只能解决一种mode。

而后我们再看CFRunLoopModeRef的结构,我已经给大家省略了里面那些我们不需要关注的变量

typedef struct __CFRunLoopMode *CFRunLoopModeRef;struct __CFRunLoopMode {    CFStringRef _name;    CFMutableSetRef _sources0;    CFMutableSetRef _sources1;    CFMutableArrayRef _observers;    CFMutableArrayRef _timers;};

根据上面这些我们大概的可以概括出来RunLoop这些相关类的关系。

runloop相关类的关系.png

CFRunLoopModeRef

由上面的源码我们可以略微总结一下这个CFRunLoopModeRef:

  1. CFRunLoopModeRef代表RunLoop的运行模式
  2. 一个RunLoop包含多个CFRunLoopModeRef,每个CFRunLoopModeRef又包含多个_sources0,_sources1,_observers,_timers。
  3. RunLoop每次只能运行一种mode,切换mode的时候,要先退出之前的mode。
  4. 假如mode中没有_sources0、_sources1、_observers、_timers,程序会立刻退出。

常用的两种Mode

kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行

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

CFRunLoopObserverRef

源码中给出了可以监听的RunLoop状态

/* Run Loop Observer Activities */typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {      // 进入RunLoop    kCFRunLoopEntry = (1UL << 0),    // 即将解决timers    kCFRunLoopBeforeTimers = (1UL << 1),    // 即将解决Sources    kCFRunLoopBeforeSources = (1UL << 2),    // 即将休眠    kCFRunLoopBeforeWaiting = (1UL << 5),    // 被唤醒    kCFRunLoopAfterWaiting = (1UL << 6),    // 退出循环    kCFRunLoopExit = (1UL << 7),    // 所有状态    kCFRunLoopAllActivities = 0x0FFFFFFFU};

具体的怎样样增加observer来监听RunLoop状态我就不贴代码了,网上一搜有很多的。

RunLoop的运行逻辑

前面我们已经理解了RunLoop相关的结构的源码,知道了RunLoop大概的数据结构,那RunLoop究竟是如何工作的呢?它的运行逻辑是什么?

我们理解过了每个mode中会存放不同的_sources0、_sources1、_observers、_timers,这些我们可以一律统称是RunLoop要解决的东西,那每一种具体对应我们理解的哪写事件呢?

Source0
触摸事件解决
performSelector:onThread:

Source1
基于系统Port(端口)的线程间通信
系统事件捕捉

Timers
NSTimer定时器
performSelector:withObject:afterDelay:

Observers
用于监听RunLoop的状态
UI刷新(BeforeWating)
Autorelease Pool (BeforWaiting)

注: UI的刷新并不是即时生效,比方说我们改变了view的backgroundColor,当执行到这行代码是并不是立刻生效,而是先记录下有这么一个任务,而后在RunLoop解决完所有的时间,进入休眠之前UI刷新。

runloop具体流程.png

这是大神总结的RunLoop的运行逻辑图,我直接拿过来用了。我们主要是看左边这部分,右边的这些标注是在源码中对应的主要方法名称。

这个图很容易了解,只有从06跳转到08这一步,单从图上看的话不是很清晰,这一块结合源码就比较明了了。第06步,假如存在Source1就直接跳转到08,在代码中使用了goto这个关键字,其实就是跳过了runloop休眠和唤醒这一部分的代码,直接跳转到了解决各种事件的这一部分。

下面我把源码做了少量删减,方便大家可以更清楚的梳理整个过程

// 这个是runloop入口函数SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */        // 通知Observers 即将进入RunLoop    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);    // 核心方法    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);    // 通知Observers 即将退出RunLoop    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);    return result;}

下面是核心方法

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {    int32_t retVal = 0;    do {                //通知Observers 即将解决Timers        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);                //通知Observers 即将解决Sources        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);                //解决Blocks        __CFRunLoopDoBlocks(rl, rlm);                //解决source0,根据返回值决定在解决一次blocks        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);        if (sourceHandledThisLoop) {            __CFRunLoopDoBlocks(rl, rlm);        }        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);                // source1相关        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {            msg = (mach_msg_header_t *)msg_buffer;            // 能否有Source1  有的话跳转到handle_msg            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {                goto handle_msg;            }        }        didDispatchPortLastTime = false;        // 通知Observers: 即将休眠        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);        //休眠        __CFRunLoopSetSleeping(rl);            //等待别的消息来唤醒当前线程        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);                __CFRunLoopUnsetSleeping(rl);                // 通知Observers: 即将醒来        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);    // 标识标识 !!!!!    handle_msg:;                __CFRunLoopSetIgnoreWakeUps(rl);        //下面根据是什么唤醒的runloop来分别解决                if (MACH_PORT_NULL == livePort) {            CFRUNLOOP_WAKEUP_FOR_NOTHING();            // handle nothing        } else if (livePort == rl->_wakeUpPort) {            CFRUNLOOP_WAKEUP_FOR_WAKEUP();            // do nothing on Mac OS        }                // 被Timer唤醒        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {            CFRUNLOOP_WAKEUP_FOR_TIMER();            // 解决Timers            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {                // Re-arm the next timer, because we apparently fired early                __CFArmNextTimerInMode(rlm, rl);            }        }        // 被Timer唤醒        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {            CFRUNLOOP_WAKEUP_FOR_TIMER();            // 解决Timers            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {                // Re-arm the next timer                __CFArmNextTimerInMode(rlm, rl);            }        }        // 被GCD唤醒        else if (livePort == dispatchPort) {            // 解决GCD相关            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);        } else {            //被Source1唤醒            //解决Source1            sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;        }        //在解决一遍BLocks        __CFRunLoopDoBlocks(rl, rlm);                        // 设置返回值 决定能否继续循环        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;}

图和源码结合来看,整个流程就清晰了很多。流程里面的有些东西不需要我们太过深入的研究,我们把这个流程掌握一下就OK了。

细节补充

第一点

我们都知道RunLoop有一个优势,那就是可以使线程在有工作的时候工作,没有工作的时候休眠,来减少占用CPU资源,提高程序性能。

这说明代码在执行到

//等待别的消息来唤醒当前线程__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

的时候,会阻塞当前的线程。但这种阻塞跟我们之前所用到过的阻塞线程不是一回事。

举个例子,我们可以使用while(1){};这句代码来阻塞线程,这句代码在底层会转换为汇编的代码,我们的线程一直在重读执行这几句代码,所以他仅仅是阻塞线程,并没有使线程休眠,我们的线程一直在工作。但是runloop,通过mach_msg使用了少量内核层的API,真的是实现了线程的休眠,让线程不再占用CPU资源。

第二点

RunLoop与线程的关系?

  1. 一个线程对应一个RunLoop对象。
  2. RunLoop默认不创立,在第一次获取的时候创立,主线程中的默认存在RunLoop也是由于在底层代码中,提前获取过一次。
  3. RunLoop储存在一个全局的字典中,线程是key,RunLoop是value。(源码中有所表现)
  4. RunLoop会在线程结束时销毁。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » RunLoop(一):源码与逻辑

发表回复