美文网首页
从源码看RunLoop - Mode

从源码看RunLoop - Mode

作者: MC3571 | 来源:发表于2017-12-05 21:41 被阅读0次

    注:RunLoop源码下载地址,下载号最大的压缩包。RunLoop的源码在CFRunLoop.h/.c两个文件中。

    1 RunLoop简介

    runloop是一个对象。这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行所有的Event Loop的逻辑。这个对象有两个版本NSRunLoop 和 CFRunLoopRef。CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

    2 Mode和Source

    先把runloop对象的源码定义贴出:

    // 对外开发的接口
    typedef struct __CFRunLoop * CFRunLoopRef
    
    // runloop对象的定义,不对外开发
    struct __CFRunLoop {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;          /* locked for accessing mode list */
        __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
        Boolean _unused;
        volatile _per_run_data *_perRunData;              // reset for runs of the run loop
        pthread_t _pthread;
        uint32_t _winthread;
        CFMutableSetRef _commonModes;
        CFMutableSetRef _commonModeItems;
        CFRunLoopModeRef _currentMode;
        CFMutableSetRef _modes;
        struct _block_item *_blocks_head;
        struct _block_item *_blocks_tail;
        CFAbsoluteTime _runTime;
        CFAbsoluteTime _sleepTime;
        CFTypeRef _counterpart;
    };
    

    需要注意的是__CFRunLoop是.C文件中不对外开放的,因此调试的时候拿到CFRunLoopRef结构体也没法看其结构。但是可以把CFRunLoopRef结构体打印出来,就可以看其结构了。
    首先说结论:RunLoop、Mode和Source/timer/observer的关系是,Runloop对象包含多个mode对象,每个mode对象又包含不同的source/timer/observer对象。
    源码怎么体现呢?可以看到在源码中有三个属性:_commonModes,_commonModeItems和_modes。其中_modes会存储该runloop所有的mode对象。_commonModes和_commonModeItems是为了处理"NSRunLoopCommonModes"。以下把NSRunLoopCommonModes简称为commonMode。commonMode不是一个实际存在mode。可以这么理解,commonMode是一份协议。
    举个例子:在主线程中NSDefaultRunLoopMode和UITrackingRunLoopMode遵守这份协议,那么_commonModes就会包含这两个mode的名称。以后只要主线程往commonMode中加source/timer/observer,另外两个mode也会自动把这些源加入到自己的对象中。
    接下来就通过源码看下如何实现这个。

    Mode和Source的内部实现

    以下是创建runloop对象的方法:

    static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
        CFRunLoopRef loop = NULL;
        CFRunLoopModeRef rlm;
        uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
        loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL);
        if (NULL == loop) {
        return NULL;
        }
        (void)__CFRunLoopPushPerRunData(loop);
        __CFRunLoopLockInit(&loop->_lock);
        loop->_wakeUpPort = __CFPortAllocate();
        if (CFPORT_NULL == loop->_wakeUpPort) HALT;
        __CFRunLoopSetIgnoreWakeUps(loop);
        // 默认情况下,commonMode“等于”kCFRunLoopDefaultMode
        loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
        loop->_commonModeItems = NULL;
        loop->_currentMode = NULL;
        loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        loop->_blocks_head = NULL;
        loop->_blocks_tail = NULL;
        loop->_counterpart = NULL;
        loop->_pthread = t;
    #if DEPLOYMENT_TARGET_WINDOWS
        loop->_winthread = GetCurrentThreadId();
    #else
        loop->_winthread = 0;
    #endif
        rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
        if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
        return loop;
    }
    

    _commonModes在Runloop初始化的时候就会创建成一个可变集合对象。并且添加一个KCFRunLoopDefaultMode进去。_commonModes只用来保存mode的名字,因此,改集合中所有元素都是CFString对象。
    _commonModeItems在初始化RunLoop初始化的时候等于NULL。在下面将看到只有往runloop的commonMode添加timer/source/observer时才会创建为可变的集合,并把这么资源放到里面。由此可以看出,commonMode的特殊性,它不是一个独立的_CFRunLoopMode的结构体对象,而是由_commonModeItems和_commonModes共同组成commonMode。
    _modes存储该runloop对象用到的所有mode对象。

    Mode对象现身

    说了这么久,让我们看下Mode对象是什么?以下代码就是Mode的定义:

    // 位于CFRunLoop.C未对外开发
    typedef struct __CFRunLoopMode *CFRunLoopModeRef;
    
    // 位于CFRunLoop.C未对外开发
    struct __CFRunLoopMode {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
        CFStringRef _name;
        Boolean _stopped;
        char _padding[3];
        CFMutableSetRef _sources0;
        CFMutableSetRef _sources1;
        CFMutableArrayRef _observers;
        CFMutableArrayRef _timers;
        CFMutableDictionaryRef _portToV1SourceMap;
        __CFPortSet _portSet;
        CFIndex _observerMask;
    #if USE_DISPATCH_SOURCE_FOR_TIMERS
        dispatch_source_t _timerSource;
        dispatch_queue_t _queue;
        Boolean _timerFired; // set to true by the source when a timer has fired
        Boolean _dispatchTimerArmed;
    #endif
    #if USE_MK_TIMER_TOO
        mach_port_t _timerPort;
        Boolean _mkTimerArmed;
    #endif
    #if DEPLOYMENT_TARGET_WINDOWS
        DWORD _msgQMask;
        void (*_msgPump)(void);
    #endif
        uint64_t _timerSoftDeadline; /* TSR */
        uint64_t _timerHardDeadline; /* TSR */
    };
    

    CFRunLoopModeRef和__CFRunLoopMode都在CFRunLoop.C定义的。可以看到苹果不允许我们自定义CFRunLoopModeRef对象添加到runloop中。我们只能通过NSRunLoop.h中有限的方法向runloop加源。说白了,苹果不想我们对runloop做过多的操作。
    可以看到这里存在四个集合对象:_sources0,_sources1,_observers和_timers。一个mode对象中所有能触发runloop的源都存在于在这四个集合中。
    通过上面的我们基本知道runloop的持有一堆CFRunLoopModeRef对象,放在自己的_modes属性中。每个CFRunLoopModeRef对象持有一堆的“源”。以及虚假的commonMode。接下来我们通过添加源到runloop对象中看,以上的结构是如何构成的。

    添加源到runloop

    首先看下有哪些方法可以添加源到runloop中。苹果给我们了三个方法,如下:

    /**
     @param rl runloop对象
     @param source 将要添加的源
     @param mode 将要存储该源的mode的名称
    */
    CF_EXPORT void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode);
    CF_EXPORT void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode);
    CF_EXPORT void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
    

    以下以timer未例把整个流程走一次:


    runloop.png

    上图流程图中注意点解释:
    注意点1:_commonModes中存的是支持commonModemode名称。将这些名称复制到新的集合中,以便在注意点2处使用;
    注意点2:CFSetApplyFunction方法的作用是对参数set中的每个item都调用__CFRunLoopAddItemToCommonModes方法。__CFRunLoopAddItemToCommonModes方法的第一参数就是set中遍历到的item,第二个是context。
    注意点3:该方法会查找名为modename的mode是否在runloop对象中,如果有就返回。如果没有,就要注意该方法第三个参数为true,那么就会创建一个名为modename的mode返回。

    整个流程:

    1. 添加timer并传三个参数:runloop对象、类型为CFRunLoopTimerRef的timer对象、modename;
    2. modename是否是kCFRunLoopCommonModes,如果是到第三步,否则直接到第8步;
    3. 将_commonModes中的字符串(参见注意点1)复制到set中,并将timer对象添加到_commonModeItems中;
    4. 调用CFSetApplyFunction,对set中的每个item调用__CFRunLoopAddItemToCommonModes方法。在__CFRunLoopAddItemToCommonModes中跳到步骤5;
    5. 查看名为modename的mode是否在runloop对象中存在,不存在则创建,得到rlm对象;
    6. 将timer对象添加到rlm对象的timers集合中;
    7. 将rlm对象添加到runloop的_modes对象中。

    CommonMode和DefaultMode的区别

    这里要区分主线程和子线程,苹果是区别处理的。

    子线程的CommonMode和DefaultMode

    子线程的情况比较简单,在runloop对象的_modes中,只有KCFRunLoopDefaultMode,没有名为"NSRunLoopCommonModes"的Mode。
    通常我们会以下两个方法来添加timer/source:

    // NSRunLoop 
    [runloop addTimer:timer forMode:NSRunLoopCommonModes];
    [runloop addPort:timer forMode:NSRunLoopCommonModes];
    

    所有通过以上方法往NSRunLoopCommonModes中添加的源,首先会被添加在_commonModeItems的集合中,如流程图中的步骤3。然后被加在_modes中KCFRunLoopDefaultMode的Mode中,如流程图中的4和5。NSRunLoopCommonModes是一个虚拟的mode,它在_modes集合和整个runloop中对象都不存在该名称的mode对象。但是需要注意的是直接往"KCFRunLoopDefaultMode"中添加的源不会填加到_commonModeItems的集合中。可以这么认为,_commonModeItems中只包含往"NSRunLoopCommonModes"这个虚拟Mode中加的源。而_modes中名为KCFRunLoopDefaultMode的Mode是一个真实存在的mode,它是NSRunLoopCommonModes和KCFRunLoopDefaultMode源的集合。
    子线程的runloop初始时只有一个KCFRunLoopDefaultMode。

    主线程的CommonMode和DefaultMode

    主线程和子线程类似。主线程runloop初始时_modes中有三个mode对象:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 、 UITrackingRunLoopMode和GSEventReceiveRunLoopMode。当往主线程的NSRunLoopCommonModes中添加源时。也会先往_commonModeItems中添加,然后会分别往_modes中的UITrackingRunLoopMode和kCFRunLoopDefaultMode都添加一份。因为UITrackingRunLoopMode和kCFRunLoopDefaultMode支持NSRunLoopCommonModes。这也就是加在commonModes中的定时器无论是否滑动都会触发。
    但是奇怪的是主线程runloop对象的_modes中真实存在一个名为"KCFRunLoopCommonModes"的Mode对象。但是该对象中source,timer和observer都是空。

    这里需要注意的是,即使把timer加在主线程的NSRunLoopCommonModes中也不一定保证timer一定会准时,因为主线程还可能运行在GSEventReceiveRunLoopMode中。甚至是我们自定义的mode,虽然这不常见。

    外传

    这里有三个方法需要注意:

    // 方法1 唯一的往_commonMode集合中添加内容的方法
    void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName)
    // 方法2 它会把多个items添加到commonMode中
    static void __CFRunLoopAddItemsToCommonMode(const void *value, void *ctx);
    // 方法3 最常用的添加源到commonMode中
    static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx);
    

    首先说明的是方法1没有地方调用。应该是苹果并不想我们通过该方法往_commonMode中加内容。该方法需要两个参数rl即要操作的runloop,modename即要添加_commonMode中的字符串。这里就不能保证_commonMode中的名称是唯一的(KCFRunLoopDefaultMode)。因为Mode是用名字区分的。如果commonMode的名称不是唯一的岂不乱了。
    方法2只在方法1中调用,也就说明,方法2也是没有地方调用的。其中valeu是要添加的源
    即source/timer/observer。ctx是一个void的数组。ctx[0]是runloop对象,ctx[1]是mode name。
    方法3是往commonMode中添加源的正常方法。value是mode name。 ctx也是是一个void
    的数组,ctx[0]是runloop对象,ctx[1]是要添加的源,即source/timer/observer。注意该方法与方法2参数的区别。

    相关文章

      网友评论

          本文标题:从源码看RunLoop - Mode

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