从源码看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.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参数的区别。

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