第一篇:RunLoop的少量理论知识

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

目录

一、什么是RunLoop
二、RunLoop和线程的关系
三、RunLoop的Mode
四、RunLoop的内部逻辑

一、什么是RunLoop


我们平时提到RunLoop主要指的是NSRunLoopCFRunLoopCFRunLoopCoreFoundation框架下基于C语言的API,而NSRunLoop则是Foundation框架下对CFRunLoop的OC封装,并未提供额外的其余功能,所以:

RunLoop其实也是一个NSRunLoop的OC对象,它的Mode里包含了一个线程需要解决的各种事件,同时也提供了一个至关重要__CFRunLoopRun函数来完成我们通常意义所说的跑圈(跑圈就是指当有事件需要解决时线程解决事件,没有事件需要解决时线程就休眠,当有事件来临时线程被唤醒解决事件,如此循环),这个函数其实就是RunLoop的核心所在,RunLoop的核心其实就是(一个do-while循环 + 线程休眠/唤醒机制),而线程休眠/唤醒机制则是RunLoop的精髓所在,是它区别于其它框架EventLoop而独特的地方,我们会在第三部分和第四部分详细详情这两点内容。

接下来我们会跳过NSRunLoop,而是直接从CFRunLoop的源码下手,从底层上来理解一下RunLoop。

二、RunLoop和线程的关系


在iOS开发中我们会遇到两个线程对象:pthead和NSThread,pthead是基于C语言的API,而NSThread则是pthead的OC封装,它们俩是逐个对应的。

而我们之所以要提到RunLoop和线程的关系,是由于RunLoop是基于pthead来管理的,具体的说,苹果并没有为我们提供直接创立RunLoop的API,开发中假如我们想要使用RunLoop,就只能通过苹果提供的CFRunLoopGetMain()CFRunLoopGetCurrent() 函数来获取一个RunLoop,而函数的内部我们其实传入了一个线程。这两个函数的内部逻辑大概如下:

// 全局区runLoopDict,用来存放Runloop,其中key就是pthead,对应的value就是Runloopstatic CFMutableDictionaryRef runLoopDict;// 获取一个线程所对应的RunloopCFRunLoopRef _CFRunLoopGet(pthread thread) {        // 第一次启动App    if (!runLoopDict) {                // 初始化全局区runLoopDict        runLoopDict = CFDictionaryCreateMutable();                // 为主线程创立一个Runloop,并把主线程和主Runloop作为一对key-value保存到runloopDict中        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());        CFDictionarySetValue(runLoopDict, pthread_main_thread_np(), mainLoop);    }        // 从runloopDict中获取指定线程所对应的Runloop    CFRunLoopRef runloop = CFDictionaryGetValue(runLoopDict, thread);    // 假如获取不到指定线程所对的Runloop    if (!runloop) {                // 就为该线程创立一个Runloop        CFRunLoopRef newRunloop = __CFRunLoopCreate(thread);        // 保存在全局区runLoopDict中        CFDictionarySetValue(runLoopDict, thread, newRunloop);        // 并返回        runloop = CFDictionaryGetValue(runLoopDict, thread);        // 同时注册一个回调,确保当线程销毁的时,顺便也销毁其对应的Runloop        _CFSetTSD(..., thread, runloop, __CFFinalizeRunLoop);    }        return loop;}

从上面的代码我们可以的出以下结论:

  • RunLoop是基于线程来管理的,它们逐个对应,共同存储在一个全局区的runLoopDict中,线程是key,RunLoop是value。

  • RunLoop的创立:主线程所对应RunLoop在程序一启动创立主线程的时候系统就会自动为我们创立好,而子线程所对应的RunLoop并不是在子线程创立出来的时候就创立好的,而是在我们获取该子线程所对应的RunLoop时才创立出来的,换句话说,假如你不获取一个子线程的RunLoop,那么它的RunLoop就永远不会被创立。

  • RunLoop的获取:我们可以通过一个指定的线程从runLoopDict中获取它所对应的RunLoop。

  • RunLoop的销毁:系统在创立RunLoop的时候,会注册一个回调,确保线程在销毁的同时,也销毁掉其对应的RunLoop。

三、RunLoop的Mode


谈到RunLoop,我们就不得不谈它的Mode,RunLoopMode是个非常重要的概念。这一部分我们会先谈一下RunLoop和RunLoopMode的关系,而后再单独谈一下RunLoopMode。

1、RunLoop和RunLoopMode的关系

我们先来看下RunLoop和RunLoopMode的关系。

struct __CFRunLoop {    CFMutableSetRef _commonModes;    CFRunLoopModeRef _currentMode;    CFMutableSetRef _modes;    ...};
  • 系统一共为RunLoop提供了两种不同的RunLoopMode(两种模式以外我们可以自己设置RunLoopMode),即DefaultMode和TrackingMode,DefaultMode是RunLoop默认的运行模式,而TrackingMode是RunLoop追踪scrollView滚动时的运行模式。

    比方说我们启动App后什么也不做,那么主线程对应的RunLoop就默认运行在DefaultMode下,此时假如我们创立一个timer(timer会被默认增加到RunLoop的DefaultMode下),timer事件会被正常触发,但假如此时我们滑动了一个scrollView,那么主线程的RunLoop就会切换到TrackingMode模式下,timer事件就无法正常被触发了。

    因而假如我们想要达到滚动scrollView时,timer事件也能被正常触发,一种办法就是创立timer的时候把它分别增加到RunLoop的DefaultMode和TrackingMode下,另一种办法则是把timer增加到RunLoop的CommonModes下。严格来说,CommonModes其实不是RunLoop一种Mode,而是少量Mode的组合,比方说系统就默认的把DefaultMode和TrackingMode增加到这种模式中了,也就是说假如我们让一个RunLoop运行在CommonModes下,并不是说RunLoop就真得是运行在CommonModes下,只是说RunLoop在切换Mode的时候会自动把原Mode的Timer/Source/Observer自动同步到目标Mode下,从而保证事件源在所有的Mode下都能正常触发

  • 每一个RunLoop都必需运行在某种特定的RunLoopMode下,所以在RunLoop的主函数RunLoopRun每次被触发之前,我们都得给当前RunLoop指定一个它要运行的RunLoopMode,也就是_currentMode成员变量,RunLoop默认运行在DefaultMode下。但这并不是一个RunLoop就只能运行一种Mode,只是同一时间只能运行一种Mode而已,比方说它的modes成员变量里存储着DefaultMode和TrackingMode,则表明RunLoop可以运行在这两种模式下,默认的时候运行DefaultMode,当滚动scrollview的时候则运行在TrackingMode下。

  • 当我们要切换一个RunLoop的Mode时,必需退出当前Mode,而后重新进入RunLoop,以确保不同Mode之间的事件源互不影响。

2、RunLoopMode

上一小节我们说完了RunLoop和RunLoopMode的关系,这一小节我们单独来看下RunLoopMode。

struct __CFRunLoopMode {    CFStringRef _name;    CFMutableArrayRef _timers;    CFMutableSetRef _sources0;    CFMutableSetRef _sources1;    CFMutableArrayRef _observers;    ...};

一个RunLoopMode内部又包含Source、Timer、Observer三种、若干个mode item,Timer和Source是RunLoop的事件源,Observer是RunLoop的观察者。

  • Source事件源

Source事件源可以分为:Source0和Source1。

Source0事件源(非基于port),主要少量负责App内部事件的解决(比方我们比方改变了一个view的frame或者者手动调用了 UIView/CALayer的setNeedsLayout/setNeedsDisplay方法时,这个布局事件就是Source0事件,会被标记为待解决,在下一个RunLoop执行时会被解决掉。),它只包含了一个回调,不能主动唤醒线程,需要先调用CFRunLoopSourceSignal(source),将这个Source标记为待解决,而后手动调用CFRunLoopWakeUp(runloop)来唤醒RunLoop的线程去执行相应的事件。

Source1事件源(基于port),主要用来解决硬件事件(例如触摸、手势、手机锁屏、静音、靠近传感器、屏幕旋转、加速感应等事件),包含了一个mach_port和一个回调,它能主动唤醒线程来执行事件。

  • Timer事件源

Timer事件源和Source1事件源一样,是一种基于port的事件源,主要负责的就是NSTimer事件。NSTimer就是基于RunLoop在工作的,因而我们在使用NSTimer的时候必需得把它增加到一个特定的RunLoop中,否则timer是起不了作用的,只需我们把timer增加到RunLoop中,RunLoop就会注册相应的时间点,一到注册的时间点,线程就会被唤醒去执行Timer事件。

  • Observer观察者

    Observer不是RunLoop的事件源,而是RunLoop的观察者,它主要用来监听RunLoop的运行状态。Observer内部有一个回调,每当RunLoop的运行状态发生变化时,Observer的这个回调就会被触发,从而接收到RunLoop运行状态的变化告知外界。RunLoop的主要运行状态有如下几个:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {        kCFRunLoopEntry         = (1UL << 0),// 即将进入RunLoop    kCFRunLoopBeforeTimers  = (1UL << 1),// 即将解决Timer事件    kCFRunLoopBeforeSources = (1UL << 2),// 即将解决Source事件    kCFRunLoopBeforeWaiting = (1UL << 5),// 即将进入休眠    kCFRunLoopAfterWaiting  = (1UL << 6),// 即将从休眠中唤醒    kCFRunLoopExit          = (1UL << 7),// 即将退出RunLoop};

四、RunLoop的内部逻辑


一个线程创立成功后,假如它对应的RunLoop也被创立成功,那么系统就会调用RunLoop的__CFRunLoopRun函数使RunLoop对象开始运行,也就是进入我们通常意义上所说的跑圈,这个函数就是RunLoop的核心所在,它的伪代码如下:

int __CFRunLoopRun(runloop, currentMode, timeout, ...) {        // 1、将要进入RunLoop,并通知RunLoop的Observers    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);        int exit = 0;    do {                // 2、RunLoop的线程将要解决Timer事件,并通知RunLoop的Observers        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);        // 3、RunLoop的线程将要解决Source0事件,并通知RunLoop的Observers        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);                // 4、RunLoop的线程解决非推迟的主线程调用        __CFRunLoopDoBlocks(runloop, currentMode);        // 5、RunLoop的线程解决Source0事件        __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);                // 6、解决完Source0事件之后,检测一下有没有Source1事件处于待解决状态,假如有就直接跳转到第10步的消息唤醒机制那里去解决Source1事件(由于Source1事件是基于port的,所以跳转到那里去解决了)        if (__Source0DidDispatchPortLastTime) {            Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)            if (hasMsg) goto handle_msg;        }                // 7、Source0事件解决完之后没有检测到Source1事件,或者者Source0事件解决完之后检测到Source1事件并且解决完了Source1事件,那么就表明此时已经没有Source事件需要解决了,通知观察者RunLoop的线程即将进入休眠        if (!sourceHandledThisLoop) {            __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);        }                // 8、与此同时,RunLoop会调用系统内核Mach的方法mach_msg()函数,来等待一个唤醒,而后线程会进入休眠,直到被被唤醒        mach_port wakeUpPort = mach_msg(msg, MACH_RCV_MSG, port);                // 9、收到唤醒事件,RunLoop的线程被唤醒,并通知RunLoop的Observers,        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);                // 10、解决唤醒事件:包括手动唤醒事件(如Source0唤醒)和基于port的唤醒事件(如Timer唤醒、Source1唤醒、异步唤醒)        if (wakeUpPort == TimerPort) {// 解决Timer唤醒事件                        __CFRunLoopDoTimers();        } else if (wakeUpPort == Source1Port) {// 解决Source1唤醒事件                        _CFRunLoopDoSource1();        } else {// 解决子线程dispatch到主线程的事件                        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);        }                // 11、退出RunLoop有三种情况        if (__CFRunLoopIsStopped(runloop)) {// RunLoop被外部调用者强行终止了                        exit = 1;        } else if (timeout) {// RunLoop超时了                        exit = 1;        } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {// RunLoop当前的Mode下没有Source/Timer/Observer了                        exit = 1;        }    } while (exit == 0);// 假如RunLoop不满足退出的条件,就继续循环        // 12. 通知Observers:RunLoop 即将退出。    __CFRunLoopDoObservers(kCFRunLoopExit);}

__CFRunLoopRun方法的内部逻辑也可以用下图来形容:


参考博客 1 : 深入了解 RunLoop
参考博客 2 : iOS刨根问底 – 深入了解 RunLoop


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

发表回复