从源码看RunLoop - Mode

注: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 - Mode_第1张图片
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)