美文网首页程序员
第一篇:RunLoop的一些理论知识

第一篇:RunLoop的一些理论知识

作者: 意一ineyee | 来源:发表于2017-09-15 10:51 被阅读48次

目录

一、什么是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就是Runloop
static CFMutableDictionaryRef runLoopDict;

// 获取一个线程所对应的Runloop
CFRunLoopRef _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


相关文章

网友评论

    本文标题:第一篇:RunLoop的一些理论知识

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