RunLoop详解

作者: 苏东没有坡 | 来源:发表于2021-06-19 23:15 被阅读0次

    写在前面

    本文仅是自己学习RunLoop的一个记录,参考了ibireme大神的 深入理解RunLoop,加入了自己的理解,时间原因还不够细致,后期慢慢丰富。

    再次感谢ibireme大神

    1. RunLoop的概念

    通常来讲,一个线程在执行完任务后,会直接退出。 但在我们日常的App使用中,即使什么都不做,App也不会退出。这其中的原理,便是在线程中构建一个消息循环,使得这个线程中一直有任务执行。在iOS和OSX中,这个消息循环的机制便被称为RunLoop

    RunLoop 可以保持程序的持续运行,它负责处理事件和消息,可以让线程在没有消息的时候休眠,在有消息的时候才唤醒做事, 以节省CPU资源。

    伪代码类似这样

    void runloop() {
       int retVal = 0;
       do {
           int message = sleep_and_wait();
           retVal = process_message(message);
       } while (0 == retVal);
    }
    

    OSX/iOS系统中,提供了2套API来访问和使用RunLoop

    CoreFoundation框架中的CFRunLoopRef对象,它提供了纯C函数的API,是线程安全的。苹果开源了CFRunLoopRef

    Foundation框架中的NSRunLoop对象,它是基于 CFRunLoopRef的封装,但是这些 API 不是线程安全的。

    2. RunLoop与线程

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

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

    // 保存RunLoop的全局字典,key 是 pthread_t, value 是 CFRunLoopRef
    static CFMutableDictionaryRef __CFRunLoops = NULL;
    
    CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
     
        if (!__CFRunLoops) {
            // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
            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);
        }
        // 从__CFRunLoops字典中取出当前传入的key对应的RunLoop
        CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
     
        if (!loop) {
            // 没有取到就创建一个 (所以子线程不获取就不创建RunLoop)
            loop = __CFRunLoopCreate(t);
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), loop);
        }
         // 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
     
        return loop;
    }
    
    

    从上面的代码可以看出

    • 线程RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。
    • 子线程刚创建时并没有RunLoop,如果你不主动获取,那它一直都不会有。
      *RunLoop 会在第一次获取时创建,在线程结束时销毁。
    • 主线程RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

    3. RunLoop 的结构

    CoreFoundation框架中有关于RunLoop的5个类

    CFRunLoopRef
    CFRunLoopModeRef
    CFRunLoopSourceRef
    CFRunLoopObserverRef
    CFRunLoopTimerRef
    
    // 对外暴露了,声明在CFRunLoop.c文件中
    typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
    typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef;
    typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopObserver * CFRunLoopObserverRef;
    typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
    
    // 没有对外暴露,声明在CFRunLoop.c文件中
    typedef struct __CFRunLoopMode *CFRunLoopModeRef;
    
    
    3.1 CFRunLoopRef 与 CFRunLoopModeRef
    // 结构体定义
    struct __CFRunLoop {
        pthread_t _pthread;  // runloop和线程是一一对应关系,每个runloop内部会保留一个对应的线程
        CFMutableSetRef _commonModes;  //标记为common的mode的集合
        CFMutableSetRef _commonModeItems;  //commonMode的item集合
        CFRunLoopModeRef _currentMode;  // 当前的模式
        CFMutableSetRef _modes; // CFRunLoopModeRef类型的集合,相对NSArray有序,Set为无序集合
    };
    
    struct __CFRunLoopMode {
        CFStringRef _name;
        CFMutableSetRef _sources0;
        CFMutableSetRef _sources1;
        CFMutableArrayRef _observers;
        CFMutableArrayRef _timers;
    };
    

    每个RunLoop中包含了多个ModeMode里面又包含了多个Source/Observer/Timer,其中source分为source0source1, 多个mode里,有且仅有一个为currentMode。如果要切换Mode,只能退出当前 Loop,再重新指定一个Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

    runloop内部modes.png

    这里有个Common的概念。我们可以将一个Mode标记为Common属性,当Mode被标记为Common时,会将该Mode添加到CommonModes里,而当Timer/Source/Observer被添加到CommonModes里面的时候,系统会遍历所有的CommonModes成员,该Timer/Source/Observer会被添加到所有CommonModes成员中,也就是该Timer/Source/Observer会被添加到CommonModeItems集合里

    主线程里默认UITrackingRunLoopMode,kCFRunLoopDefaultMode被标记了Common,子线程中默认只有kCFRunLoopDefaultMode被标记。

    我们可以通过CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode)将一个Mode标记为Common

    image.png
    3.2 CFRunLoopSourceRef

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

    • Source0 只包含了一个回调 ,它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
    • Source1包含了一个 mach_port 和一个回调 ,被用于通过内核和其他线程相互发送消息。这种Source 能主动唤醒RunLoop 的线程.
    struct __CFRunLoopSource {
        CFRuntimeBase _base; 
        uint32_t _bits;  //用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被触发
        pthread_mutex_t _lock;
        CFIndex _order;         /* immutable */
        CFMutableBagRef _runLoops;
        union {
            CFRunLoopSourceContext version0;     // source0
            CFRunLoopSourceContext1 version1;    // source1
        } _context;
    };
    
    typedef struct {
        CFIndex version;
        void *  info;
        void    (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
        void    (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
        void    (*perform)(void *info);
    } CFRunLoopSourceContext;
    
    typedef struct {
        CFIndex version;
        void *  info;
        mach_port_t (*getPort)(void *info);
        void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
     
    } CFRunLoopSourceContext1;
     
    
    3.3 CFRunLoopTimerRef

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

    从下面的结构体中可以看到,结构体内部有一个runloop成员,它是timer所在的RunLoop,一个timer仅能够添加到一个RunLoop中去,一旦timer被添加到RunLoop中,再有其他的RunLoop添加该timer,其实是无效的。

    struct __CFRunLoopTimer {
        CFRuntimeBase _base;
        uint16_t _bits; //标记fire状态
        pthread_mutex_t _lock;
        CFRunLoopRef _runLoop; //添加该timer的runloop
        CFMutableSetRef _rlModes; //存放所有 包含该timer的 mode的 modeName,意味着一个timer可能会在多个mode中存在
        CFAbsoluteTime _nextFireDate; // 下一次触发时间
        CFTimeInterval _interval;    //执行的时间间隔
        CFTimeInterval _tolerance;   //时间偏差
        uint64_t _fireTSR;      // 触发时间
        CFIndex _order;         /* immutable */
        CFRunLoopTimerCallBack _callout;    // 回调
        CFRunLoopTimerContext _context;  // 回调内容
    };
    
    3.4 CFRunLoopObserverRef

    CFRunLoopObserverRef是观察者,和CFRunLoopTimerRef一样,它的结构体内部包含着一个_runloop成员, 表明每个Observer同时只能监听一个RunLoop, 每个 Observer 都包含了一个回调,当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

    /* Run Loop Observer Activities */
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),
        kCFRunLoopBeforeTimers = (1UL << 1),
        kCFRunLoopBeforeSources = (1UL << 2),
        kCFRunLoopBeforeWaiting = (1UL << 5),
        kCFRunLoopAfterWaiting = (1UL << 6),
        kCFRunLoopExit = (1UL << 7),
        kCFRunLoopAllActivities = 0x0FFFFFFFU
    };
    
    struct __CFRunLoopObserver {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;  // 线程锁
        CFRunLoopRef _runLoop; // 所在的runloop
        CFIndex _rlCount; // Observer监控的runloop数量 逻辑主要在__CFRunLoopObserverSchedule和__CFRunLoopObserverCancel
        CFOptionFlags _activities;      // RunLoop的几个状态 - immutable
        CFIndex _order;         /* immutable */
        CFRunLoopObserverCallBack _callout; // 回调
        CFRunLoopObserverContext _context;  // 回调的参数
    };
    
    

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

    4. RunLoop 对外的接口

    CFRunLoop对外暴露的管理 Mode接口只有下面2个:

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

    你只能通过mode name来操作内部的 mode,当你传入一个新的mode nameRunLoop内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个RunLoop来说,其内部的 mode 只能增加不能删除。

    苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode)UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode

    5. RunLoop 内部流程图

    SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    
        // 通知Observers, 进入RunLoop
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
        // RunLoop里面具体要做的事情, 主要是一个循环
        result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
        // 通知Observers, 退出 RunLoop
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
        return result;
    }
    
      
    static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
        
        
        mach_port_name_t dispatchPort = MACH_PORT_NULL;
     
        // 当前是否为主线程
        bool cond1 = pthread_main_np();
        // _CFGetTSD(__CFTSDKeyIsInGCDMainQ) 从预先分配的插槽中获取主线程的一些特定数据
        // CF源码中没有搜索到_CFSetTSD(__CFTSDKeyIsInGCDMainQ)的调用,推测但不完全确定_CFGetTSD(__CFTSDKeyIsInGCDMainQ) 为空
        bool cond2 = 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ);
        // 当前是否为主线程
        bool cond3 = CFRunLoopGetMain() == rl;
        // 当前的mode包含在commonModes里面
        bool cond4 = CFSetContainsValue(rl->_commonModes, rlm->_name);
     
        // 当前为主线程且mode被标记为common,dispatchPort才会被赋值
        if (cond1 && cond2 && cond3 && cond4) {
            dispatchPort = _dispatch_get_main_queue_port_4CF();
        }
    
     
        Boolean didDispatchPortLastTime = true;
    
        int32_t retVal = 0;
        do {
    
            __CFRunLoopUnsetIgnoreWakeUps(rl);
    
            // 通知observers, 即将处理Timers
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
            // 通知observers, 即将处理Sources
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
            // 处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
    
            // 处理source0
            if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
                // 处理Blocks
                __CFRunLoopDoBlocks(rl, rlm);
            }
    
            // timeout 传入0 表示立即返回 传入TIMEOUT_INFINITY 表示等待到消息再返回
            
            // 当前为主线程且mode被标记为common,且非第一次循环
            if (dispatchPort && !didDispatchPortLastTime) {
                // 主线程里有消息处理,直接跳转到handle_msg
                if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                    goto handle_msg;
                }
            }
            didDispatchPortLastTime = false;
     
            // 通知observers, 即将进入休眠
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
            __CFRunLoopSetSleeping(rl);
    
            // 线程进入休眠,等待唤醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
    
            __CFRunLoopUnsetSleeping(rl);
            // 通知observers, 已经结束休眠
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
    
            // 处理消息
            handle_msg:;
    
            if (msg_is_timer) {
                // 处理timer
                __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
            } else if (msg_is_dispatch) {
                // 处理GCD  如果有dispatch到main_queue的block,执行block
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } else {
                // 处理Source1
                __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
     
            }
     
            // 处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
            
    
              // 设置返回值
           if (sourceHandledThisLoop && stopAfterHandle) {
              //stopAfterHandle 是传入的标记,如果处理完事件就返回
              retVal = kCFRunLoopRunHandledSource;
           } else if (timeout_context->termTSR < mach_absolute_time()) {
               //设置了超时时间,超时返回
              retVal = kCFRunLoopRunTimedOut;
           } else if (__CFRunLoopIsStopped(rl)) {
                // 被外部调用者强制停止了
              __CFRunLoopUnsetStopped(rl);
             retVal = kCFRunLoopRunStopped;
           } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
              // 内部source,observers,timers 都空了,返回
              retVal = kCFRunLoopRunFinished;
           }
            
        // 如果不进入上述的条件,继续循环
        } while (0 == retVal);
    
        if (timeout_timer) {
            dispatch_source_cancel(timeout_timer);
            dispatch_release(timeout_timer);
        } else {
            free(timeout_context);
        }
    
        return retVal;
    }
    
    

    流程图示如下:


    RunLoop内部流程示意.png

    6. RunLoop 的底层实现

    从RunLoop的源码中可以看到,RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。为了解释这个逻辑,下面稍微介绍一下 OSX/iOS 的系统架构。

    RunLoop_3

    苹果官方将整个系统大致划分为上述4个层次:
    应用层包括用户能接触到的图形应用,例如 Spotlight、Aqua、SpringBoard 等。
    应用框架层即开发人员接触到的 Cocoa 等框架。
    核心框架层包括各种核心框架、OpenGL 等内容。
    Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其所有源码都可以在 opensource.apple.com 里找到。

    我们在深入看一下 Darwin 这个核心的架构:

    RunLoop_4

    其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。
    XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。
    BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。
    IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。

    Mach 本身提供的 API 非常有限,而且苹果也不鼓励使用 Mach 的 API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为”对象”。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”消息”是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。

    Mach 的消息定义是在 <mach/message.h> 头文件的,很简单:

    typedef struct{
        mach_msg_header_t       header;
        mach_msg_body_t         body;
    } mach_msg_base_t;
    
    typedef struct{
        mach_msg_bits_t       msgh_bits;
        mach_msg_size_t       msgh_size;
        mach_port_t           msgh_remote_port;
        mach_port_t           msgh_local_port;
        mach_port_name_t      msgh_voucher_port;
        mach_msg_id_t         msgh_id;
    } mach_msg_header_t;
    
    

    一条 Mach 消息实际上就是一个二进制数据包 (BLOB),其头部定义了当前端口 local_port 和目标端口 remote_port,
    发送和接受消息是通过同一个 API 进行的,其 option 标记了消息传递的方向:

    /*
     *  Routine:    mach_msg
     *  Purpose:
     *      Send and/or receive a message.  If the message operation
     *      is interrupted, and the user did not request an indication
     *      of that fact, then restart the appropriate parts of the
     *      operation silently (trap version does not restart).
     */
    __WATCHOS_PROHIBITED __TVOS_PROHIBITED
    extern mach_msg_return_t        mach_msg(
        mach_msg_header_t *msg,
        mach_msg_option_t option,
        mach_msg_size_t send_size,
        mach_msg_size_t rcv_size,
        mach_port_name_t rcv_name,
        mach_msg_timeout_t timeout,
        mach_port_name_t notify);
    

    为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作,如下图:

    RunLoop_5

    这些概念可以参考维基百科: System_callTrap_(computing)

    RunLoop 的核心就是一个 mach_msg() ,RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。

    关于具体的如何利用 mach port 发送信息,可以看看 NSHipster 这一篇文章,或者这里的中文翻译 。

    关于Mach的历史可以看看这篇很有趣的文章:Mac OS X 背后的故事(三)Mach 之父 Avie Tevanian

    相关文章

      网友评论

        本文标题:RunLoop详解

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