Objective-C基础之八(深入理解RunLoop)

2,406 阅读20分钟

什么是RunLoop?

RunLoop其实是一个事件处理循环,被用作工作调度并且协调传入事件的接收。一般情况下,单条线程一次只能执行一个任务,执行完成之后线程就会退出,如果我们希望线程能够随时的处理事件并且不会退出,那么就在线程中开启一个RunLoop,RunLoop其实就是一个运行循环,它的主要目的是能够让线程在有工作的时候保持忙碌,在没有工作的时候进入休眠,这样做的好处就是让线程进入休眠之后避免资源占用。

RunLoop的结构如下

RunLoop在循环过程中处理定时器事件、performSelector事件、自定义源以及端口事件等等。

RunLoop的整个运行逻辑其实可以归纳为以下代码

function loop() {
    initialize();
    do {
        //休眠中等待消息
        var message = get_next_message();
        //处理消息
        process_message(message);
    } while (message != quit);
}

由此可见,其实RunLoop就是一个对象,它管理了需要进行处理的事件和消息,并且为线程提供了一个如上的入口函数,当线程执行了这个入口函数之后,就会进入一个“接收消息->休眠等待->处理消息”的循环之中,一直到循环结束。

Cocoa和Core Foundation框架都提供了运行循环对象来帮助我们配置和管理线程的运行循环:NSRunLoop和CFRunLoopRef

  • CFRunLoopRef是Core Foundation框架提供的,是纯C函数的API,所有相关的API都是线程安全的。源码可以参考CF源码下载
  • NSRunLoop则是对CFRunLoopRef进行了封装,是面向对象的,它不是线程安全的。

RunLoop的主要作用可以总结为以下几点:

  • 能够保持程序持续运行(主线程RunLoop默认开启)
  • 处理App中的各种事件(触摸事件、定时器)
  • 节省CPU资源,提高程序性能(有消息就处理消息,无消息就进入休眠状态,不占用资源)

RunLoop和线程的联系

运行循环其实不是完全自动的,当我们创建一个线程的时候需要在适当的时间启动运行循环并且响应传入的事件,上文中说到Cocoa和Core Foundation框架都提供了运行循环对象来帮助我们配置和管理线程的运行循环,因此我们不需要显式的创建运行循环对象,程序中的每个线程,包括应用程序的主线程都有一个关联的RunLoop对象。但是,只有子线程需要显式的开启它们的运行循环。而主线程作为应用程序启动过程的一部分,应用程序框架会在主线程上自动设置并且运行RunLoop。

苹果的API为我们提供了两种方法获取RunLoop对象

  • 在Cocoa框架中,我们使用[NSRunLoop mainRunLoop]获取主线程的RunLoop对象,使用[NSRunLoop currentRunLoop]获取当前线程的RunLoop对象。
  • 在Core Foundation框架中,我们使用CFRunLoopGetMain()获取主线程的RunLoop对象,使用CFRunLoopGetCurrent()获取当前线程的RunLoop对象。

阅读CF源码,我们可以得到获取RunLoop函数的内部逻辑如下

//获取线程对应的RunLoop,如果t=0表示获取主线程的RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    //如果当前线程为nil
    if (pthread_equal(t, kNilPthreadT)) {
        //获取主线程
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
        //第一次进入时,全局字典__CFRunLoops为nil,所以需要初始化全局dic
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //首先为主线程创建一个RunLoop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        //将主线程RunLoop存入全局字典中
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
	if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
	    CFRelease(dict);
	}
	CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    //根据线程t到全局字典__CFRunLoops中获取对应的RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
        //如果不存在对应RunLoop,则为当前线程t新建一个RunLoop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            //并且将新建的RunLoop保存到全局字典中去
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
        __CFUnlock(&loopsLock);
        CFRelease(newLoop);
    }
    
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            // 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

由此可见线程和RunLoop的关系如下

  • 线程和RunLoop之间一一对应,每条线程都有唯一一个与之对应的RunLoop对象
  • RunLoop保存在一个全局的字典里,线程作为Key,RunLoop对象作为value
  • 线程刚创建时并没有RunLoop对象,RunLoop对象会在第一次获取它的时候进行创建
  • RunLoop会在线程结束的时候进行销毁
  • 主线程的RunLoop自动创建并启动,子线程默认没有开启RunLoop,并且只能在线程内部获取其RunLoop,主线程除外。

RunLoop底层结构

在Core Foundation框架中提供了关于RunLoop的5个类

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

它们之间的关系如下:

在一个RunLoop对象当中包含了多个Mode,而每个Mode又包含了若干个Source0/Source1/Timer/Observer,RunLoop启动的时候只能指定其中的一个Mode,这个Mode被称为CurrentMode。如果需要切换Mode,只能先退出当前Loop,然后重新指定Mode再进入Loop。这样做的主要目的就是为了分割不同组的Source0/Source1/Timer/Observer,让它们互不影响。

如果Mode中没有任何的Source0/Source1/Timer/Observer,那么RunLoop会立即退出。这里的Source0/Source1/Timer/Observer统称为一个Mode item。

CFRunLoopRef

CFRunLoopRef其实就是Core Foundation框架提供的RunLoop对象。

CFRunLoopModeRef

CFRunLoopModeRef其实就是就是多个Source0/Source1/Timer/Observer的集合。每次运行RunLoop循环时,都要指定特定的模式,在RunLoop循环过程当中,只监视与该模式相关联的源,并且允许它们进行事件交付。类似的,也只有关联了该模式的观察者才能监听到RunLoop的状态变化。CFRunLoopModeRef的大致结构如下

struct __CFRunLoopMode {
    CFStringRef _name;              //Mode名称
    Boolean _stopped;
    char _padding[3];   
    CFMutableSetRef _sources0;      //Source0
    CFMutableSetRef _sources1;      //Source1
    CFMutableArrayRef _observers;   //观察者集合
    CFMutableArrayRef _timers;      //定时器集合
    ......
}

struct __CFRunLoop {
    __CFPort _wakeUpPort;			     // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData; // reset for runs of the run loop
    CFMutableSetRef _commonModes;        // 所有标记为Common的Mode的name集合       
    CFMutableSetRef _commonModeItems;    // 所有被标记为Common的Source/Observer/Timer
    CFRunLoopModeRef _currentMode;       // 当前RunLoop指定的Mode
    CFMutableSetRef _modes;              // 所有Mode集合
    ......
};

Mode中主要包含以下几种元素:

  • Mode的名称
  • Source0的集合
  • Source1的集合
  • 观察者集合observers
  • 定时器的集合timers

苹果提供了两种公开的Mode,kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和UITrackingRunLoopMode

  • NSDefaultRunLoopMode是RunLoop默认的Mode,主线程的RunLoop一般情况下都会处于这种状态。
  • UITrackingRunLoopMode是用于追踪UIScrollView滑动的一种Mode,当程序监听到ScrollView滑动时,RunLoop会切换到此Mode下

而在RunLoop对象中,集合_modes包含了所有RunLoop支持的Mode。同时,RunLoop还支持一种“CommonModes”的概念,每个Mode都能将其标记为“Common”属性(具体是将Mode的name添加到RunLoop的_commonModes集合当中),主线程的RunLoop两个预置的Mode:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和UITrackingRunLoopMode都已经被标记为了“Common”。

RunLoop中的_commonModeItems集合就是用来存放被标记为common的Source/Observer/Timer,当RunLoop状态发生变化时,会将_commonModeItems中的所有的Source/Observer/Timer同步到具有common标识的所有Mode中。

以定时器举例,我们在主线程中添加一个定时器,并且添加到NSDefaultRunLoopMode当中,定时器会正常回调。此时如果界面上存在ScrollView,并且滑动ScrollView,RunLoop就会切换到UITrackingRunLoopMode模式下,此时,由于定时器只存在于NSDefaultRunLoopMode模式中,一旦切换到UITrackingRunLoopMode模式,定时器便会停止,等待RunLoop重新切换到DefaultMode时恢复运行。如果想要定时器在两种模式下都能够正常运行,可以将定时器同时添加到两种Mode中,还有一种方式,就是将定时器标记为“common”,其实也就是将定时器加入到RunLoop对象的_commonModeItems中去。此时RunLoop会自动将定时器同步到具有Common标记的Mode中去。代码如下:

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"定时器任务");
}];
//方法一
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
//方法二
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

除了使用苹果公开的Mode外,我们还可以创建自定义Mode,具体接口如下

//指定RunLoop创建Mode,主要是通过mode name来创建,如果RunLoop内部根据mode name没有发现对应的mode,RunLoop则会自动创建CFRunLoopModeRef
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

同时,Mode中也提供了对Mode Item的操作函数,如下

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

CFRunLoopSourceRef

CFRunLoopSourceRef又称之为输入源,它是事件产生的地方。输入源是将事件异步传递到线程中。事件的源则取决于输入源的类型,而输入源一般分为两类:第一类是自定义输入源,监视自定义事件源,又称为Source0。第二类是基于端口的输入源,监视应用程序的Mach端口,又称为Source1

  • Source0 只包含一个回调,它不能主动触发事件,使用Source0时,需要先调用CFRunLoopSourceSignal(source)将Source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒RunLoop,让它来处理这个事件。简单来说,Source0其实就是负责App内部事件的处理(例如UITouch事件等等)
  • Source1包含了一个mach_port和一个回调指针,被用于通过内核与其它线程进行通信。Source1能够主动唤醒RunLoop的线程。简单来说,Source1其实就是用来接收系统发出的事件(例如手机的触摸、摇晃或者锁屏等等)

举一个简单的例子:当App在前台静止时,如果我们点击App的页面,此时我们首先接触的是手机屏幕,此时,触摸屏幕的事件会被包装成Event传递给source1,然后source1主动唤醒RunLoop,之后将事件传递给Source0来进行处理。

CFRunLoopTimerRef

CFRunLoopTimerRef是定时器源,它会在将来某个预设的时间点将事件同步发送到线程。定时器是线程通知自己做某件事的一种方式。定时器生成基于时间的通知,但是它并不是实时的。与输入源一样,计时器和运行循环的特定mode相关联。

  • 如果计时器没有处于运行循环当前监听的模式中,那么定时器不会被触发,直到运行循环切换到定时器支持的运行模式。
  • 类似的,如果定时器在运行循环正在处理程序任务时触发,那么定时器会等待下一次运行循环时触发。
  • 如果线程没有开启RunLoop,定时器也不会触发。

我们可以配置定时器来生成一次或者多次事件,重复定时器会根据预定的触发时间(而不是实际的触发时间)自动重新调度自己。例如一个定时器在一个特定的时间触发,并且在之后每5s触发一次,那么计划的触发时间将始终落在最初的5s间隔上。如果此时RunLoop正在处理一个耗时的任务,那么定时器的触发时间会被延时。假设RunLoop执行的耗时任务为12s,那么在RunLoop执行完完耗时任务之后,定时器会立即触发一次,然后定时器会重新安排下一次预定的触发时间。也就是说在耗时的12s内,只会触发一次定时器。

CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,每一个Observer都包含一个回调,当RunLoop状态发生变化时,观察者就能够通过回调接收到变化状态。RunLoop有以下几种状态能被观测

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),           //即将进入loop
    kCFRunLoopBeforeTimers = (1UL << 1),    //即将处理Timer
    kCFRunLoopBeforeSources = (1UL << 2),   //即将处理Source
    kCFRunLoopBeforeWaiting = (1UL << 5),   //即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),    //从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),            //即将退出loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

在代码中,我们可以使用以下方式来监听RunLoop的状态变化

CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:{
                NSLog(@"即将进入loop");
                break;
            }
            case kCFRunLoopBeforeTimers:{
                NSLog(@"即将处理Timer");
                break;
            }
            case kCFRunLoopBeforeSources:{
                NSLog(@"即将处理Source");
                break;
            }
            case kCFRunLoopBeforeWaiting:{
                NSLog(@"即将进入休眠");
                break;
            }
            case kCFRunLoopAfterWaiting:{
                NSLog(@"从休眠中唤醒");
                break;
            }
            case kCFRunLoopExit:{
                NSLog(@"即将退出loop");
                break;
            }
            default:
                break;
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);

RunLoop运行逻辑

RunLoop运行逻辑流程图如下:

  1. 通知Observers:即将进入loop
  2. 通知Observers:即将处理Timers
  3. 通知Observers:即将处理Source0
  4. 开始处理Source0
  5. 如果此时有Source1,则跳到第9步
  6. 通知Observers:线程即将休眠
  7. 让线程休眠,直到以下时机唤醒
    • 接收到Source1
    • 启动Timer
    • 为运行循环设置超时值过期
    • 被外部手动唤醒
  8. 通知Observers:线程刚刚被唤醒
  9. 接收到消息,开始处理挂起事件
    • 如果用户自定义的Timer触发,则处理Timer事件并重新启动循环,跳转到步骤2
    • 如果此时触发了输入源,则传递事件
    • 如果运行循环被手动唤醒,但是尚未超时,则重新启动循环,跳转到步骤2
  10. 通知Observers:退出loop

此处是对照官方文档的流程,其中忽略了对block的处理

RunLoop源码

想要查看RunLoop源码,首先需要知道RunLoop的入口函数,方法很简单,新建项目,在项目启动后在任意处断点,然后通过LLDB指令bt可以得到调用栈如下:

其中CFRunLoopRunSpecific函数就是RunLoop的入口函数,如下(此处只保留部分主要代码):

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    //第一步、通知Observers:即将进入loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    //内部函数,进入loop
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //第十步、通知Observers:退出loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    return result;
}

loop的真正核心就是__CFRunLoopRun函数,源码如下:

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    
    Boolean didDispatchPortLastTime = true;
    int32_t retVal = 0;
    do {
        //第二步、通知Observers:即将处理Timers
         __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        //第三步、通知Observers:即将处理Source0(非port)
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        //执行被加入的block
        __CFRunLoopDoBlocks(rl, rlm);
        
        //第四步、处理Source0,触发Source0回调
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            //再次执行执行被加入的block
            __CFRunLoopDoBlocks(rl, rlm);
        }

        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        //第五步、判断是否有Source1需要进行处理,如果有,跳转到handle_msg标记处执行
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
            msg = (mach_msg_header_t *)msg_buffer;
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                goto handle_msg;
            }
        }
        didDispatchPortLastTime = false;
        //第六步、通知Observers,RunLoop所在线程即将进入休眠
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
            
        //第七步、调用内核函数mach_msg,让线程进入休眠,直到被以下事件唤醒
        //1、接收到Source1事件
        //2、启动Timer
        //3、为运行循环设置超时值过期
        //4、被外部手动唤醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
    
        //第八步、通知Observers:RunLoop线程刚被唤醒
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

    //第九步、接收到消息开始进行处理
    handle_msg:;
        if (被Timer唤醒) {
            //9-1、如果用户自定义的计时器触发,则处理计时器事件并重新启动循环
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()
        }
        else if (被GCD唤醒) {
            //9-2、如果GCD子线程中有回到主线程的操作,那么唤醒线程,执行主线程的block
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else {
            //9-3、如果有基于port的Source1事件传入,则处理Source1事件
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            if (rls) {
                mach_msg_header_t *reply = NULL;
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
                if (NULL != reply) {
                    (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);                }
            }
        }
        //再次执行被加入的block
        __CFRunLoopDoBlocks(rl, rlm);
            
        if (sourceHandledThisLoop && stopAfterHandle) {
            //如果当前事件处理完成就直接返回
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            //RunLoop到超时时间了
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
            //RunLoop被强制停止了
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            //如果RunLoop当前指定的Mode中Source/Timer/Observer都为空
            retVal = kCFRunLoopRunFinished;
        }
        //如果RunLoop没有超时,没有被停止,指定的Mode中存在ModeItem,则一直运行RunLoop
        } while (0 == retVal);

        return retVal;
}

RunLoop回调

RunLoop在进行回调时,都会调用一个特别长的函数,例如调用__CFRunLoopDoObservers通知Observers即将进入RunLoop时,内部会调用CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION函数。以下将源码中对应的函数转换成了实际调用的函数,如下

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    //第一步、通知Observers:即将进入loop
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    //内部函数,进入loop
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //第十步、通知Observers:退出loop
     __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
    
    return result;
}

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

    Boolean didDispatchPortLastTime = true;
    int32_t retVal = 0;
    do {
        //第二步、通知Observers:即将处理Timers
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        //第三步、通知Observers:即将处理Source0(非port)
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        //执行被加入的block
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
        
        //第四步、处理Source0,触发Source0回调
        Boolean sourceHandledThisLoop = __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        if (sourceHandledThisLoop) {
            //再次执行执行被加入的block
            __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
        }

        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        //第五步、判断是否有Source1需要进行处理,如果有,跳转到handle_msg标记处执行
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
            msg = (mach_msg_header_t *)msg_buffer;
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                goto handle_msg;
            }
        }
        didDispatchPortLastTime = false;
        //第六步、通知Observers,RunLoop所在线程即将进入休眠
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
            
        //第七步、调用内核函数mach_msg,让线程进入休眠,直到被以下事件唤醒
        //1、接收到Source1事件
        //2、启动Timer
        //3、为运行循环设置超时值过期
        //4、被外部手动唤醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
    
        //第八步、通知Observers:RunLoop线程刚被唤醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

    //第九步、接收到消息开始进行处理
    handle_msg:;
        if (被Timer唤醒) {
            //9-1、如果用户自定义的计时器触发,则处理计时器事件并重新启动循环
            __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
        }
        else if (被GCD唤醒) {
            //9-2、如果GCD子线程中有回到主线程的操作,那么唤醒线程,执行主线程的block
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
        } else {
            //9-3、如果有基于port的Source1事件传入,则处理Source1事件
            __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
        }
        //再次执行被加入的block
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
            
        //如果RunLoop没有超时,没有被停止,指定的Mode中存在ModeItem,则一直运行RunLoop
        } while (0 == retVal);
        
        return retVal;
}

具体流程图总结如下:

流程中穿插着对block的处理,其中的block可以通过CFRunLoopPerformBlock函数来添加。

RunLoop应用

线程保活

RunLoop一个很重要的作用就是能用来控制线程的生命周期,也就是线程包活。主线程的RunLoop默认开启,因此主线程一直处于活跃状态,但是子线程默认没有开启RunLoop,所以子线程执行完任务之后就会被销毁。早期AFNetworking 2.x版本就使用了RunLoop来保活线程。具体实现如下:

XLThread.h源码如下

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef void(^XLThreadTask)(void);

@interface XLThread : NSObject

/** 开始任务 */
- (void)executeTask:(XLThreadTask)task;

/** 结束线程 */
- (void)stop;

@end

NS_ASSUME_NONNULL_END

XLThread.m如下

#import "XLThread.h"
#import <objc/runtime.h>

@interface XLThread ()

@property(nonatomic, strong)NSThread *innerThread;

@end

@implementation XLThread

#pragma mark - Public
- (instancetype)init
{
    self = [super init];
    if (self) {
        
        self.innerThread = [[NSThread alloc] initWithTarget:self selector:@selector(__initThread) object:nil];
        [self.innerThread start];
    }
    return self;
}

- (void)executeTask:(XLThreadTask)task{
    if (!self.innerThread || !task) {
        return;
    }
    [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}


- (void)stop
{
    if (!self.innerThread) return;
    
    [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}

- (void)dealloc
{
    SBPLog(@"%s", __func__);
    
    [self stop];
}

#pragma mark - Private
- (void)__stop{
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}

- (void)__executeTask:(XLThreadTask)task{
    task();
}

- (void)__initThread{
    //创建上下文
    CFRunLoopSourceContext context = {0};
    //创建source
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    //向runloop中添加source
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    //销毁source
    CFRelease(source);
    //启动runLoop
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
}

@end

AutoreleasePool

在iOS中,AutoreleasePool其实也是基于RunLoop来实现的,App启动时会在主线程的RunLoop中注册两个Observer。

  • 第一个Observer用来监听RunLoop的Entry事件,在监听到Entry事件时会调用_objc_autoreleasePoolPush()来创建自动释放池。
  • 第二个Observer监听两种事件
    • 当RunLoop准备进入休眠(处于BeforeWaiting状态)时,会调用_objc_autoreleasePoolPop()来释放旧的池,然后调用_objc_autoreleasePoolPush()来创建新的自动释放池
    • 当退出RunLoop(处于Exit状态)时,会调用_objc_autoreleasePoolPop()来释放AutoreleasePool

关于AutoreleasePool的具体实现会在后面的章节详细介绍。

NSTimer

前面说到的CFRunLoopTimerRef其实就是NSTimer,NSTimer的触发必须基于RunLoop,并且RunLoop需要处于开启状态。通常我们会在主线程中使用定时器,因为主线程的RunLoop默认开启,如果想要在子线程中使用定时器,就需要手动开启子线程RunLoop。

当我们创建NSTimer,然后将其注册到RunLoop后,RunLoop会根据预设的触发时间在重复的时间点注册好事件,例如设置定时器在5:10分开始触发,并且每隔5m触发一次,定时器的触发时间就是固定的5:10、5:15、5:20、5:25等等,但是RunLoop为了节省资源,并不会在非常准确的时间点触发定时器。如果在触发定时器之前RunLoop执行了一个很长的任务,以至于错过了预设的时间点,那么在长时间任务执行完成之后会立即触发一次定时器,然后等到下一个预设的时间点再次触发。

假设原定于5:10分触发定时器,但是RunLoop由于执行耗时任务到5:22,那么此时,在耗时任务执行完成之后会立即触发一次定时器任务,然后等待到5:25时再次触发定时器。

PerformSelector

上文提到,RunLoop中输入源分为两种,一种是基于port的输入源,还有一种是自定义输入源。

Cocoa框架为我们提供了一种自定义源(Perform Selector Source),可以让我们在任何线程上执行Selector,并且在执行完Selector之后,该源会自动从RunLoop中移除。当我们使用performSelector在另一个线程中执行Selector时,必须要保证目标线程中有开启RunLoop,因此就需要我们显式的启动目标线程的RunLoop。

以下就是在NSObject中声明的performSelector方法

方法 描述
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
在应用程序主线程的RunLoop的下一次循环周期内执行指定的Selector。
并且该方法可以阻塞当前线程,直到执行Selector为止。
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
在任何线程上指向指定的Selector。
并且该方法可以阻塞当前线程,直到执行Selector为止。
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
在下一个运行循环周期中以及可选的延迟时间之后,在当前线程上执行指定的selector。
因为是等到下一个运行循环周期执行selector,所以这些方法提供了当前执行代码的最小自动延迟。
多个排队的选择器按照排队的顺序依次执行
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
取消发送到当前线程的消息

举一个简单的例子,在touchesBegan:方法中添加以下示例代码

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"任务1");
    [self performSelector:@selector(test) withObject:nil afterDelay:.0];
    NSLog(@"任务3");
}

- (void)test{
    NSLog(@"任务2");
}

当点击页面时,执行顺序依次是任务1、任务3和任务2。原因很简单,因为performSelector:withObject:afterDelay方法本质其实是创建一个定时器NSTimer,并且将定时器添加到RunLoop中进行处理。整个流程大致如下:

  • 首先点击屏幕,其实就是接收到一个Source1事件,由Source1唤醒RunLoop,然后将事件传递给Source0
  • RunLoop被唤醒之后会重新进入循环,因此首先会处理Source0事件,因此任务1和任务3会被执行
  • 任务2则是通过添加NSTimer的方式来执行,虽然是延迟0s,但是始终是定时器事件,而RunLoop是在被唤醒时才会去处理NSTimer事件,因此任务2是在RunLoop下一次被唤醒时才会被执行。

参考文章

RunLoop官方文档

深入理解RunLoop | Garan no dou

结束语

以上内容纯属个人理解,如果有什么不对的地方欢迎留言指正。

一起学习,一起进步~~~