RunLoop 运行机制原理逻辑与GCD及线程关系剖析

前言

文章主要会RunLoop源码进行剖析,里面会有对它的理解及注释,有不足望见解

1,RunLoop是什么?

广义上的来说,run loop 就是所谓的 event loop,或者称之为「事件循环」或者「事件分发器」。Event loop 是 event-driven programming(事件驱动编程)非常重要的组成部分,而事件驱动编程则是 GUI 程序的最常见编程方式(现在似乎在服务器端也有很多应用,但在 GUI 编程方面肯定是绕不过去)。

Event Loop

Run Loop 是一个 iOS 开发里的基础概念,它并非独有的机制,很多系统和框架都有类似的实现,Run Loop 是 Event Loop (事件循环)机制的在 iOS 平台的一种实现。

Event loop 的思想非常简单,用下面的伪代码来表示:

'hello'

int main(void){

    初始化();

    while(message !=退出){

        处理事件(message);

        message =获取下一个事件();

    }

    return 0;

}

```

苹果官网文档:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html

Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

Run loop management is not entirely automatic. You must still design your thread’s code to start the run loop at appropriate times and respond to incoming events. Both Cocoa and Core Foundation provide run loop objects to help you configure and manage your thread’s run loop. Your application does not need to create these objects explicitly; each thread, including the application’s main thread, has an associated run loop object. Only secondary threads need to run their run loop explicitly, however. The app frameworks automatically set up and run the run loop on the main thread as part of the application startup process.

The following sections provide more information about run loops and how you configure them for your application. For additional information about run loop objects, see NSRunLoop Class Reference and CFRunLoop Reference.

运行循环是与线程相关的基础架构的一部分。一个运行循环是指用于安排工作,并协调接收传入事件的事件处理循环。运行循环的目的是在有工作时保持线程忙,并在没有线程时让线程进入休眠状态。

运行循环管理不是完全自动的。您仍然必须设计线程的代码以在适当的时间启动运行循环并响应传入的事件。Cocoa和Core Foundation都提供了运行循环对象来帮助您配置和管理线程的运行循环。您的应用程序不需要显式创建这些对象; 每个线程(包括应用程序的主线程)都有一个关联的运行循环对象。但是,只有辅助线程需要显式运行其运行循环。作为应用程序启动过程的一部分,应用程序框架会自动在主线程上设置并运行运行循环。

以下部分提供有关运行循环以及如何为应用程序配置它们的更多信息。有关运行循环对象的其他信息,请参阅NSRunLoop类参考CFRunLoop参考

Figure 3-1 shows the conceptual structure of a run loop and a variety of sources. The input sources deliver asynchronous events to the corresponding handlers and cause the runUntilDate: method (called on the thread’s associated NSRunLoop object) to exit. Timer sources deliver events to their handler routines but do not cause the run loop to exit.

图3-1   运行循环的结构及其来源



回到 macOS/iOS 平台上,对于 event loop 的具体实现有两个:

Foundation 框架中的 NSRunLoop

Core Foundation 框架中的 CFRunLoop

其中 NSRunLoop 是对 CFRunLoop 的简单封装,需要着重研究的只有 CFRunLoop。

2,Run Loop 实现

网上目前有关 Run Loop 的文章, 10 篇里面可能有 8 篇都是重复了 深入理解RunLoop 中的代码。包括本人阅读不下5遍,在阅读runloop源码前后都去阅读YY作者文章,然而这都是经过作者大量简化过的版本,隐藏了大量的细节。

RunLoop 与线程的关系

首先,iOS 开发中能遇到两个线程对象: pthread_t 和 NSThread。过去苹果有份文档标明了 NSThread 只是 pthread_t 的封装,但那份文档已经失效了,现在它们也有可能都是直接包装自最底层的 mach thread。苹果并没有提供这两个对象相互转换的接口,但不管怎么样,可以肯定的是 pthread_t 和 NSThread 是一一对应的。比如,你可以通过 pthread_main_thread_np() 或 [NSThread mainThread] 来获取主线程;也可以通过 pthread_self() 或 [NSThread currentThread] 来获取当前线程。CFRunLoop 是基于 pthread 来管理的。

苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:

'''

CFRunLoopGetMain :

CFRunLoopRefCFRunLoopGetMain(void) {

    CHECK_FOR_FORK();

    static CFRunLoopRef __main = NULL; // no retain needed

    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed

    return__main;

}

CFRunLoopRefCFRunLoopGetCurrent(void) {

    CHECK_FOR_FORK();

    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);

    if(rl)returnrl;

    return _CFRunLoopGet0(pthread_self());

}

'''

CFRunLoopGet0

无论是 CFRunLoopGetMain 还是 CFRunLoopGetCurrent ,两者调用了 CFRunLoopGet0 :

// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef

'''

static CFMutableDictionaryRef __CFRunLoops = NULL;

/// 访问 loopsDic 时的锁

staticCFLock_t loopsLock = CFLockInit;

// should only be called by Foundation

// t==0 is a synonym for "main thread" that always works

/// 获取一个 pthread 对应的 RunLoop。

CF_EXPORTCFRunLoopRef_CFRunLoopGet0(pthread_tt) {

    if(pthread_equal(t,kNilPthreadT)) {

t =pthread_main_thread_np();

    }

    __CFLock(&loopsLock);

    if (!__CFRunLoops) {

         // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。

        __CFUnlock(&loopsLock);

CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);

CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());

CFDictionarySetValue(dict,pthreadPointer(pthread_main_thread_np()), mainLoop);

if(!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void*volatile*)&__CFRunLoops)) {

    CFRelease(dict);

}

CFRelease(mainLoop);

        __CFLock(&loopsLock);

    }

    /// 直接从 Dictionary 里获取。

    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));

    __CFUnlock(&loopsLock);

    if(!loop) {

        //直接创建一个

CFRunLoopRef newLoop = __CFRunLoopCreate(t);

        __CFLock(&loopsLock);

loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));

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

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

        }

    }

    returnloop;

}

'''

从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

CHECK_FOR_FORK()

在两个函数里,都有使用了 CHECK_FOR_FORK() 。

它应该是属于多进程情况下的一个断言。

Threading Programming Guide 中,有这么一段话:

Warning: When launching separate processes using the fork function, you must always follow a call to fork with a call to exec or a similar function. Applications that depend on the Core Foundation, Cocoa, or Core Data frameworks (either explicitly or implicitly) must make a subsequent call to an exec function or those frameworks may behave improperly.

也就是说,当通过 fork 启动一个新进程的时候,你必须要接着调用一个 exec 或类似的函数。而依赖于 Core Founadtion / Cocoa / Core Data 框架的应用,必须调用 exec 函数,否则这些框架也许不能正确的工作。

所以为了保证安全,使用 CHECK_FOR_FORK 进行检查。

FORK

这里简单提一下 fork 。

在 UNIX 中,用 fork 来创建子进程,调用 fork( ) 的进程被称为父进程,新进程是子进程,并且几乎是父进程的完全复制(变量、文件句柄、共享内存消息等相同,但 process id 不同)。

因为子进程和父进程基本是一样的,要想让子进程去执行其他不同的程序,子进程就需要调用 exec ,把自身替换为新的进程,其中process id不变,但原来进程的代码段、堆栈段、数据段被新的内容取代,来执行新的程序。

这样 fork 和 exec 就成为一种组合。

而在 iOS 这样的类 UNIX 系统里,基本上也都要通过 fork 的形式来创建新的进程。

假如没有执行完 exec ,那么执行的代码段等内容,还是父进程里的,出现问题可以说百分之百。这就是 CHECK_FOR_FORK 检查的目的。

RunLoop 对外的接口

在 CoreFoundation 里面关于 RunLoop 有5个类:

CFRunLoopRef

CFRunLoopModeRef

CFRunLoopSourceRef

CFRunLoopTimerRef

CFRunLoopObserverRef

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:


一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。

• Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

• Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

'''

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

};

'''

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

RunLoop 的内部逻辑

根据苹果在文档里的说明,RunLoop 内部的逻辑大致如下:


官方文档:

The Run Loop Sequence of Events

Each time you run it, your thread’s run loop processes pending events and generates notifications for any attached observers. The order in which it does this is very specific and is as follows:

Notify observers that the run loop has been entered.

Notify observers that any ready timers are about to fire.

Notify observers that any input sources that are not port based are about to fire.

Fire any non-port-based input sources that are ready to fire.

If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.

Notify observers that the thread is about to sleep.

Put the thread to sleep until one of the following events occurs:

An event arrives for a port-based input source.

A timer fires.

The timeout value set for the run loop expires.

The run loop is explicitly woken up.

Notify observers that the thread just woke up.

Process the pending event.

If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.

If an input source fired, deliver the event.

If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.

Notify observers that the run loop has exited.

/// 用DefaultMode启动

'''

void CFRunLoopRun(void) {

    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);

}

'''

/// 用指定的Mode启动,允许设置RunLoop超时时间

'''

int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {

    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);

}

'''

以下是官方源码 并没有删减代码,这样大家更好理解点

'''

SInt32CFRunLoopRunSpecific(CFRunLoopRefrl,CFStringRefmodeName,CFTimeIntervalseconds,BooleanreturnAfterSourceHandled) {    /* DOES CALLOUT */

    CHECK_FOR_FORK();

     //检查 run loop 是否正在销毁

    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;

    __CFRunLoopLock(rl);

    // 查找 modeName 指定的 mode

    CFRunLoopModeRefcurrentMode =__CFRunLoopFindMode(rl, modeName,false);

    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {// 没有找到 mode 或者 mode 里面没有任何事件源的话,返回 kCFRunLoopRunFinished

        //这里比较奇怪的是 Boolean did = false 直接写死了 did 的值,后面又是 return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished . 怀疑 did 的值,应该还有一段代码是决定kCFRunLoopRunHandledSource的结果,被苹果隐藏了没有开源出来

Booleandid =false;

if(currentMode)__CFRunLoopModeUnlock(currentMode);

__CFRunLoopUnlock(rl);

return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;

    }

    // 因为可以嵌套调用,保存一下之前的状态

    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);

    CFRunLoopModeRef previousMode = rl->_currentMode;

    rl->_currentMode= currentMode;

    int32_t result = kCFRunLoopRunFinished;

if(currentMode->_observerMask&kCFRunLoopEntry)__CFRunLoopDoObservers(rl, currentMode,kCFRunLoopEntry);

result =__CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);

if(currentMode->_observerMask&kCFRunLoopExit)__CFRunLoopDoObservers(rl, currentMode,kCFRunLoopExit);

        __CFRunLoopModeUnlock(currentMode);

        __CFRunLoopPopPerRunData(rl, previousPerRun);

rl->_currentMode= previousMode;

    __CFRunLoopUnlock(rl);

    returnresult;

}

/**

 *  运行run loop

 *

 *  @param rl              运行的RunLoop对象

 *  @param rlm            运行的mode

 *  @param seconds        run loop超时时间

 *  @param stopAfterHandle true:run loop处理完事件就退出  false:一直运行直到超时或者被手动终止

 *  @param previousMode    上一次运行的mode

 *

 *  @return 返回4种状态

 */

/* rl, rlm are locked on entrance and exit */

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

    //mach_absolute_time is a CPU/Bus dependent function that returns a value based on the number of "ticks" since the system started up.

    // mach_absolute_time  是一个CPU/总线依赖函数,返回一个基于系统启动后的时钟"嘀嗒"数。在macOS上可以确保它的行为,并且,它包含系统时钟所包含的所有时间区域。其可获取纳秒级的精度

    //获取系统启动之后cpu嘀嗒数

    uint64_tstartTSR =mach_absolute_time();


    if (__CFRunLoopIsStopped(rl)) {

        __CFRunLoopUnsetStopped(rl);

        return kCFRunLoopRunStopped;

    }elseif(rlm->_stopped) {

        rlm->_stopped =false;

        return kCFRunLoopRunStopped;

    }

    /*

     kern_return_t mach_port_names 

     (ipc_space_t                                task,

     mach_port_name_array_t                  * names,

     mach_msg_type_number_t                * namesCnt,

     mach_port_type_array_                    * types,

     mach_msg_type_number_t                * typesCnt );


     mach_port_name_t must be an unsigned type.  Port values

     *  have two parts, a generation number and an index.

     *  These macros encapsulate all knowledge of how

     *  a mach_port_name_t is laid out.  They are made visible 

     *  to user tasks so that packages to map from a mach_port_name_t

     *  to associated user data can discount the generation

     *  nuber (if desired) in doing the mapping.

     *

     *  Within the kernel, ipc/ipc_entry.c implicitly assumes

     *  when it uses the splay tree functions that the generation

     *  number is in the low bits, so that names are ordered first

     *  by index and then by generation.  If the size of generation

     *  numbers changes, be sure to update IE_BITS_GEN_MASK and

     *  friends in ipc/ipc_entry.h.


     返回有关任务端口名称空间的信息。

     该mach_port_names返回有关任务的端口名称空间。它返回任务的当前活动名称,表示某些端口,端口集或死命名。对于每个名称,它还返回任何类型的权限 任务(由mach_port_type返回的相同信息)。


     请注意,当对mach_port_names的调用返回时,两个输出数组(名称和类型)中的条目数相等(namesCnt等于typesCnt)。此接口返回两个单独计数的事实是Mach Interface Generator的工件。

     task

     查询端口名称空间的任务。

     names

     [指向动态数组mach_port_name_t的指针]任务端口名称空间中的端口,端口集和死名称的名称,没有特定的顺序。

     namesCnt

     [out scalar]返回的名称数量。

     types

     [指向动态数组mach_port_type_t的指针]每个相应名称的类型。指示任务使用该名称保留的权限类型。

     typesCnt

     [out scalar]返回的类型数。

     返回值

     仅适用一般性错误。

     */

    //mach 端口, 线程之间通信的对象  mach端口,内核进程通信消息端口。初始为0

    mach_port_name_tdispatchPort =MACH_PORT_NULL;

    //pthread_main_np() 获取主线程 这里主要是为了判断当前线程是否为主线程 

    // 检测是否在主线程 && ( (是队列发的消息&&mode为null)||(不是队列发的消息&&不在主队列))

    BooleanlibdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY &&NULL== previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY &&0== _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));

    /*

     需要在主线程,run loop 也是主线程的 run loop,并且 mode 是 common mode

     从 GCD 的私有 API 获取端口(4CF 表示 for Core Foundation)

     则给 dispatchPort 赋值为主线程收发消息的端口

     */

    //如果是队列安全的,并且是主线程runloop,设置它对应的通信端口

    /*

     _dispatch_get_main_queue_port_4CF是 dispatch_get_main_queue_handle_4CF 的宏,存在 libdispatch 中,里面对它的实现为:


     dispatch_runloop_handle_t

     _dispatch_get_main_queue_handle_4CF(void)

     {

     dispatch_queue_t dq = &_dispatch_main_q;

     dispatch_once_f(&_dispatch_main_q_handle_pred, dq,

     _dispatch_runloop_queue_handle_init);

     return _dispatch_runloop_queue_get_handle(dq);

     }

     返回的是主线程 runloop 所关联的的端口。

     */

    if(libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort =_dispatch_get_main_queue_port_4CF();

    /*

     USE_DISPATCH_SOURCE_FOR_TIMERS 这个宏的值为 1,也就是说有使用 GCD 来实现 timer,当然 USE_MK_TIMER_TOO 这个宏的值也是 1,表示也使用了更底层的 timer。

     */

    //如果使用 GCD timer 作为 timer 的实现的话,进行准备工作

#if USE_DISPATCH_SOURCE_FOR_TIMERS

    //  MACOSX 下,声明一个 mode 的队列通信端口(在 MACOSX 环境中):

    mach_port_name_t modeQueuePort = MACH_PORT_NULL;

    if(rlm->_queue) {

        modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);

        if(!modeQueuePort) {

            CRASH("Unable to get port for run loop mode queue (%d)", -1);

        }

    }

#endif

    //使用GCD实现runloop超时功能 

    //GCD管理的定时器,用于实现runloop超时机制

    dispatch_source_ttimeout_timer =NULL;

    //创建上下文 跟GCD 定时器关联一起

    struct__timeout_context *timeout_context = (struct__timeout_context *)malloc(sizeof(*timeout_context));


    if (seconds <= 0.0) { // instant timeout//立即超时

        seconds =0.0;

        timeout_context->termTSR =0ULL;

    }elseif(seconds <= TIMER_INTERVAL_LIMIT) {// seconds为超时时间,超时时执行__CFRunLoopTimeout函数 

        //根据是否为主线程,设置队列是主队列还是后台队列

        dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();

        timeout_timer =dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0,0, queue);

        dispatch_retain(timeout_timer);

        timeout_context->ds = timeout_timer;

        //runloop

        timeout_context->rl = (CFRunLoopRef)CFRetain(rl);

        timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);

        /*

         void dispatch_set_context(dispatch_object_t object, void *context);

         将应用程序定义的上下文与对象相关联。

         您的应用程序可以将自定义上下文数据与对象相关联,仅供您的应用程序使用。您的应用程序必须根据需要分配和取消分配数据。

         object

         这个参数不能NULL。

         context

         对象的新应用程序定义的上下文。这可以NULL。


         void dispatch_source_set_event_handler_f(dispatch_source_t source, dispatch_function_t handler);

         为给定的调度源设置事件处理函数。

         source

         调度源要修改。这个参数不能NULL。

         handler

         事件处理函数提交到源的目标队列。传递给事件处理函数的context参数是处理程序调用时调度源的当前上下文。这个参数不能NULL。

         */

        dispatch_set_context(timeout_timer, timeout_context);// source gets ownership of context

        dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);

        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);

        uint64_tns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) *1000000000ULL);

        dispatch_source_set_timer(timeout_timer,dispatch_time(1, ns_at),DISPATCH_TIME_FOREVER,1000ULL);

        //恢复唤起 timer 执行

        dispatch_resume(timeout_timer);

    }else { // infinite timeout // 永不超时

        //无限期超时

        seconds =9999999999.0;

        timeout_context->termTSR =UINT64_MAX;

    }


    //设置超时timer 结束  标志位默认为true

    // 设置判断是否为最后一次 dispatch 的端口通信的变量

    BooleandidDispatchPortLastTime =true;

    //记录最后runloop状态,用于return 退出 runloop

    // 设置一个结果变量,最后为几个 CFRunLoopRunInMode 里返回状态之一。

    int32_tretVal =0;

    do{

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI

        // 一个状态变量,用于 消息状态 标志,初始值为 UNCHAMGED

        voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED;

        voucher_t voucherCopy =NULL;

#endif

        // 初始化一个存放内核消息的缓冲池

        uint8_t msg_buffer[3*1024];

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI

        //声明和 mach port 有关的 port 和 msg 变量

        mach_msg_header_t *msg =NULL;

        // 活动端口,本地用来接收消息的端口

        mach_port_t livePort = MACH_PORT_NULL;

#endif

        // 取所有需要监听的port,runloopMode

        // 声明一个类型为 CFPortSet 的 waitSet, 值为 run loop mode 里的 portSet.

        __CFPortSet waitSet = rlm->_portSet;


        // 设置RunLoop为可以被唤醒状态

        //将 run loop 从忽略唤醒消息的状态 unset ,开始接受唤醒消息

        __CFRunLoopUnsetIgnoreWakeUps(rl);


        //2. 通知 Observers: RunLoop 即将触发 Timer 回调

        if(rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

        //3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。

        if(rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);


        // 执行被加入的block

        __CFRunLoopDoBlocks(rl, rlm);


        //4. RunLoop 触发 Source0 (非port) 回调。

        // 执行 Source0 (非 mach port) 。 有事件处理返回 true,没有事件返回 false

        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

        if(sourceHandledThisLoop) {

            // 执行加入当前 runloop 的 block

            __CFRunLoopDoBlocks(rl, rlm);

        }


        // 如果没有 Sources0 事件处理 并且 没有超时,poll 为 false

        // 如果有 Sources0 事件处理 或者 超时,poll 都为 true

        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);


        //5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息

        // 第一次do..whil循环不会走该分支,因为 didDispatchPortLastTime 初始化是 true

        if(MACH_PORT_NULL!= dispatchPort && !didDispatchPortLastTime) {

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI

            // 从缓冲区读取消息

            msg = (mach_msg_header_t *)msg_buffer;

            // 5. 接收 dispatchPort 端口的消息,dispatch 到 main queue 的事件。

            if(__CFRunLoopServiceMachPort(dispatchPort, &msg,sizeof(msg_buffer), &livePort,0, &voucherState,NULL)) {

                // 如果接收到了消息的话,前往 handle_msg 开始处理 msg

                gotohandle_msg;

            }

#endif

        }


        didDispatchPortLastTime =false;


        //6, 通知 Observers: RunLoop 的线程即将进入休眠(sleep)

        // 注意到如果实际处理了 source0 或者超时,不会进入睡眠,所以不会通知。

        if(!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);

        //设置标志位,正在睡眠(实际上没有开始睡)

        // 设置RunLoop为休眠状态

        // 设置标志位, Run Loop 休眠

        __CFRunLoopSetSleeping(rl);

        // do not do any user callouts after this point (after notifying of sleeping)


        // Must push the local-to-this-activation ports in on every loop

        // iteration, as this mode could be run re-entrantly and we don't

        // want these ports to get serviced.


        //使用 GCD 的话,将 GCD 端口加入所有监听端口集合中

        __CFPortSetInsert(dispatchPort, waitSet);


        __CFRunLoopModeUnlock(rlm);

        __CFRunLoopUnlock(rl);


        //使用 GCD 的话,将 GCD 端口加入所有监听端口集合中

        // 休眠开始的时间,根据 poll 状态决定为 0 或者当前的绝对时间

        CFAbsoluteTime sleepStart = poll ?0.0: CFAbsoluteTimeGetCurrent();


        // 这里有个内循环,用于接收等待端口的消息

        // 进入此循环后,线程进入休眠,直到收到新消息才跳出该循环,继续执行run loop

        //// 等待被唤醒,可以被 sources 1、timers、CFRunLoopWakeUp 函数和 GCD 事件(如果在主线程)

        //7.通过 CFRunLoopServiceMachPort 调用 mach_msg 休眠,等待被 mach_msg 消息唤醒

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI

        // 如果在 MACOSX 中

#if USE_DISPATCH_SOURCE_FOR_TIMERS

        // 处理 GCD timer 

        do{

            // 使用 GCD timer 作为 timer 实现的情况

            if (kCFUseCollectableAllocator) {//假如有kCFUseCollectableAllocator分配器,使用 memset 清空msg_buffer

                // objc_clear_stack(0);

                // <rdar://problem/16393959>

                // 清空 msg_buffer

                memset(msg_buffer,0,sizeof(msg_buffer));

            }

            msg = (mach_msg_header_t *)msg_buffer;

            // 这个函数会睡眠线程 waitSet: 监听端口集合  livePort: 返回收到消息的端口 poll: 根据状态睡眠或者不睡

            // 接收waitSet端口的消息

            // 设置 mach port 通信,会睡眠线程

            __CFRunLoopServiceMachPort(waitSet, &msg,sizeof(msg_buffer), &livePort, poll ?0: TIMEOUT_INFINITY, &voucherState, &voucherCopy);

            // 如果是 timer 端口唤醒的,进行一下善后处理,之后再处理 timer

            // 收到消息之后,livePort 的值为本地接收消息的活动端口

            // modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);

            //modeQueuePort:如果使用 GCD timer 作为 timer 的实现的话,进行准备工作

            //livePort 作为本地接受端口

            // modeQueue 存在,而且为 livePort

            if(modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {

                // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.

                //执行 run loop mode 里的队列,直到队列都执行完成

                while(_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));

                if (rlm->_timerFired) {//timer 唤醒的//假如 _timerFired 为真,把 livePort 作为队列端口,在之前服务于 timers

                    // Leave livePort as the queue port, and service timers below

                    rlm->_timerFired =false;

                    break;

                }else {// _timerFired 为假, 并且 msg 存在不为 msg_buffer, 释放 msg

                    if(msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);

                }

            }else{

                // Go ahead and leave the inner loop.

                // 不是 timer 端口唤醒的,进行接下来的处理

                break;

            }

        }while(1);

#else// 不在 MACOSX 中

        // 不使用 GCD timer 作为 timer 实现的情况

        if (kCFUseCollectableAllocator) {//如果 kCFUseCollectableAllocator 分配器,使用 memset 清空 msg_buffer

            // objc_clear_stack(0);

            // <rdar://problem/16393959>

            memset(msg_buffer,0,sizeof(msg_buffer));

        }

        /// . 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。

        /// • 一个基于 port 的Source 的事件。

        /// • 一个 Timer 到时间了

        /// • RunLoop 自身的超时时间到了

        /// • 被其他什么调用者手动唤醒

        msg = (mach_msg_header_t *)msg_buffer;

        //CFRunLoopServiceMachPort 会让线程休眠

        __CFRunLoopServiceMachPort(waitSet, &msg,sizeof(msg_buffer), &livePort, poll ?0: TIMEOUT_INFINITY, &voucherState, &voucherCopy);

#endif

        //上锁

        __CFRunLoopLock(rl);

        __CFRunLoopModeLock(rlm);


        // 增加记录的睡眠时间

        // 根据 poll 的值,记录休眠时间,休眠时间差

        rl->_sleepTime += (poll ?0.0: (CFAbsoluteTimeGetCurrent() - sleepStart));


        // Must remove the local-to-this-activation ports in on every loop

        // iteration, as this mode could be run re-entrantly and we don't

        // want these ports to get serviced. Also, we don't want them left

        // in there if this function returns.


        // 将 GCD 端口移除

        //对 waitSet 里的 dispatchPort 端口做移除

        __CFPortSetRemove(dispatchPort, waitSet);

        // 设置 runloop 不可被唤醒

        //让 Run Loop 忽略唤醒消息,因为已经重新在运行了

        __CFRunLoopSetIgnoreWakeUps(rl);


        //  取消runloop的休眠状态

        // user callouts now OK again

        __CFRunLoopUnsetSleeping(rl);

        // 8 通知 observers: kCFRunLoopAfterWaiting, 即停止等待(被唤醒)

        // 注意实际处理过 source 0 或者已经超时的话,不会通知(因为没有睡)

        if(!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);


        // 11. 被什么唤醒就处理什么: 处理通过端口收到的消息

    handle_msg:;

        // 设置 runloop 不可被唤醒

        //将 Run Loop 重新忽略唤醒消息,因为已经重新在运行了

        __CFRunLoopSetIgnoreWakeUps(rl);


        if (MACH_PORT_NULL == livePort) {// 不知道哪个端口唤醒的(或者根本没睡),啥也不干  livePort 为空,什么事都不做

            CFRUNLOOP_WAKEUP_FOR_NOTHING();

            // handle nothing

        }else if (livePort == rl->_wakeUpPort) {// 被 CFRunLoopWakeUp 函数弄醒的,啥也不干 跳回2重新循环 // livePort 等于 run loop 的 _wakeUpPort

            // 被 CFRunLoopWakeUp 函数唤醒的

            CFRUNLOOP_WAKEUP_FOR_WAKEUP();

            // do nothing on Mac OS

        }

#if USE_DISPATCH_SOURCE_FOR_TIMERS// 在 MACOSX 里

        elseif(modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {//被 timers 唤醒,处理 timers 

            //livePort 等于 modeQueuePort

            //9.1-1 被 timers 唤醒,处理 timers

            CFRUNLOOP_WAKEUP_FOR_TIMER();

            if(!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {

                // Re-arm the next timer, because we apparently fired early

                __CFArmNextTimerInMode(rlm, rl);

            }

        }

#endif

#if USE_MK_TIMER_TOO

        elseif(rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {// 被 模mk timers 唤醒,处理 timers

            //livePort 等于 run loop mode 的 _timerPort

            // 9.1-2 被 timers 唤醒,处理 timers

            CFRUNLOOP_WAKEUP_FOR_TIMER();

            // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.

            // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754

            if(!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {

                // Re-arm the next timer

                __CFArmNextTimerInMode(rlm, rl);

            }

        }

#endif

        else if (livePort == dispatchPort) {//9.2 如果有dispatch到main_queue的block,执行block。

            // 被 GCD 唤醒或者从第 7 步跳转过来的话,处理 GCD

            CFRUNLOOP_WAKEUP_FOR_DISPATCH();

            __CFRunLoopModeUnlock(rlm);

            __CFRunLoopUnlock(rl);

            //设置 CFTSDKeyIsInGCDMainQ 位置的 TSD 为 6 .

            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void*)6,NULL);

            // 执行block

            // 处理 msg

            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);

            //设置 CFTSDKeyIsInGCDMainQ 位置的 TSD 为 0.

            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void*)0,NULL);

            __CFRunLoopLock(rl);

            __CFRunLoopModeLock(rlm);

            //设置变量

            sourceHandledThisLoop =true;

            didDispatchPortLastTime =true;

        }else{

            // source1 事件 

            //被 source (基于 mach port) 唤醒

            CFRUNLOOP_WAKEUP_FOR_SOURCE();

            // If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again.

            // 假如我们 从这个 mach_msg 中接收到一个 voucher,然后在 TSD 中放置一个复制的新的 voucher.

            // CFMachPortBoost 会在 TSD 中去查找这个 voucher. 

            // 通过使用 TSD 中的值,我们将 CFMachPortBoost 绑定到这个接收到的 mach_msg 中,在这两段代码之间没有任何机会再次设置凭证

            voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void*)voucherCopy, os_release);

            // 被 sources 1 唤醒,处理 sources 1

            //9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件

            // Despite the name, this works for windows handles as well

            // 根据接收消息的 port 寻找 source1 事件

            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);

            if (rls) {// 有 source1 事件待处理  //如果 rls 存在

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI

                mach_msg_header_t *reply =NULL;

                // 处理 source1 事件  //处理 Source ,并返回执行结果

                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;

                if(NULL!= reply) {//发送reply消息(假如 reply 不为空)

                    (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size,0, MACH_PORT_NULL,0, MACH_PORT_NULL);

                    //释放 reply 变量

                    CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);

                }

#endif

            }

            // Restore the previous voucher

            _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);

        } 

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI

        if(msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);

#endif

        //12. 再一次处理 blocks

        __CFRunLoopDoBlocks(rl, rlm);

        // 13. 判断是否退出,不需要退出则跳转回第 2 步  //根据一次循环后的状态,给 retVal 赋值 。状态不变则继续循环

        if(sourceHandledThisLoop && stopAfterHandle) {

            retVal = kCFRunLoopRunHandledSource;

        }elseif(timeout_context->termTSR < mach_absolute_time()) {

            retVal = kCFRunLoopRunTimedOut;

        }elseif(__CFRunLoopIsStopped(rl)) {

            __CFRunLoopUnsetStopped(rl);

            retVal = kCFRunLoopRunStopped;

        }elseif(rlm->_stopped) {

            rlm->_stopped =false;

            retVal = kCFRunLoopRunStopped;

        }elseif(__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {

            retVal = kCFRunLoopRunFinished;

        }


#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI

        // 循环一次后收尾处理

        voucher_mach_msg_revert(voucherState);

        os_release(voucherCopy);

#endif

    }while(0== retVal);

    if(timeout_timer) {//如果存在,取消并释放

        dispatch_source_cancel(timeout_timer);

        dispatch_release(timeout_timer);

    }else {//不存在,将对应的 timeour_context 释放

        free(timeout_context);

    }

    //结束返回 retVal 状态。

    return retVal;

}

'''

可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容