從源碼看RunLoop - Mode

注:RunLoop源碼下載地址,下載號最大的壓縮包。RunLoop的源碼在CFRunLoop.h/.c兩個(gè)文件中。

1 RunLoop簡介

runloop是一個(gè)對象。這個(gè)對象管理了其需要處理的事件和消息,并提供了一個(gè)入口函數(shù)來執(zhí)行所有的Event Loop的邏輯。這個(gè)對象有兩個(gè)版本NSRunLoop 和 CFRunLoopRef。CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的,它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的。NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向?qū)ο蟮?API,但是這些 API 不是線程安全的。

2 Mode和Source

先把runloop對象的源碼定義貼出:

// 對外開發(fā)的接口
typedef struct __CFRunLoop * CFRunLoopRef

// runloop對象的定義,不對外開發(fā)
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文件中不對外開放的,因此調(diào)試的時(shí)候拿到CFRunLoopRef結(jié)構(gòu)體也沒法看其結(jié)構(gòu)。但是可以把CFRunLoopRef結(jié)構(gòu)體打印出來,就可以看其結(jié)構(gòu)了。
首先說結(jié)論:RunLoop、Mode和Source/timer/observer的關(guān)系是,Runloop對象包含多個(gè)mode對象,每個(gè)mode對象又包含不同的source/timer/observer對象。
源碼怎么體現(xiàn)呢?可以看到在源碼中有三個(gè)屬性:_commonModes,_commonModeItems和_modes。其中_modes會存儲該runloop所有的mode對象。_commonModes和_commonModeItems是為了處理"NSRunLoopCommonModes"。以下把NSRunLoopCommonModes簡稱為commonMode。commonMode不是一個(gè)實(shí)際存在mode??梢赃@么理解,commonMode是一份協(xié)議。
舉個(gè)例子:在主線程中NSDefaultRunLoopMode和UITrackingRunLoopMode遵守這份協(xié)議,那么_commonModes就會包含這兩個(gè)mode的名稱。以后只要主線程往commonMode中加source/timer/observer,另外兩個(gè)mode也會自動把這些源加入到自己的對象中。
接下來就通過源碼看下如何實(shí)現(xiàn)這個(gè)。

Mode和Source的內(nèi)部實(shí)現(xiàn)

以下是創(chuàng)建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);
    // 默認(rèn)情況下,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初始化的時(shí)候就會創(chuàng)建成一個(gè)可變集合對象。并且添加一個(gè)KCFRunLoopDefaultMode進(jìn)去。_commonModes只用來保存mode的名字,因此,改集合中所有元素都是CFString對象。
_commonModeItems在初始化RunLoop初始化的時(shí)候等于NULL。在下面將看到只有往runloop的commonMode添加timer/source/observer時(shí)才會創(chuàng)建為可變的集合,并把這么資源放到里面。由此可以看出,commonMode的特殊性,它不是一個(gè)獨(dú)立的_CFRunLoopMode的結(jié)構(gòu)體對象,而是由_commonModeItems和_commonModes共同組成commonMode。
_modes存儲該runloop對象用到的所有mode對象。

Mode對象現(xiàn)身

說了這么久,讓我們看下Mode對象是什么?以下代碼就是Mode的定義:

// 位于CFRunLoop.C未對外開發(fā)
typedef struct __CFRunLoopMode *CFRunLoopModeRef;

// 位于CFRunLoop.C未對外開發(fā)
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定義的??梢钥吹教O果不允許我們自定義CFRunLoopModeRef對象添加到runloop中。我們只能通過NSRunLoop.h中有限的方法向runloop加源。說白了,蘋果不想我們對runloop做過多的操作。
可以看到這里存在四個(gè)集合對象:_sources0,_sources1,_observers和_timers。一個(gè)mode對象中所有能觸發(fā)runloop的源都存在于在這四個(gè)集合中。
通過上面的我們基本知道runloop的持有一堆CFRunLoopModeRef對象,放在自己的_modes屬性中。每個(gè)CFRunLoopModeRef對象持有一堆的“源”。以及虛假的commonMode。接下來我們通過添加源到runloop對象中看,以上的結(jié)構(gòu)是如何構(gòu)成的。

添加源到runloop

首先看下有哪些方法可以添加源到runloop中。蘋果給我們了三個(gè)方法,如下:

/**
 @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未例把整個(gè)流程走一次:


runloop.png

上圖流程圖中注意點(diǎn)解釋:
注意點(diǎn)1:_commonModes中存的是支持commonModemode名稱。將這些名稱復(fù)制到新的集合中,以便在注意點(diǎn)2處使用;
注意點(diǎn)2:CFSetApplyFunction方法的作用是對參數(shù)set中的每個(gè)item都調(diào)用__CFRunLoopAddItemToCommonModes方法。__CFRunLoopAddItemToCommonModes方法的第一參數(shù)就是set中遍歷到的item,第二個(gè)是context。
注意點(diǎn)3:該方法會查找名為modename的mode是否在runloop對象中,如果有就返回。如果沒有,就要注意該方法第三個(gè)參數(shù)為true,那么就會創(chuàng)建一個(gè)名為modename的mode返回。

整個(gè)流程:

  1. 添加timer并傳三個(gè)參數(shù):runloop對象、類型為CFRunLoopTimerRef的timer對象、modename;
  2. modename是否是kCFRunLoopCommonModes,如果是到第三步,否則直接到第8步;
  3. 將_commonModes中的字符串(參見注意點(diǎn)1)復(fù)制到set中,并將timer對象添加到_commonModeItems中;
  4. 調(diào)用CFSetApplyFunction,對set中的每個(gè)item調(diào)用__CFRunLoopAddItemToCommonModes方法。在__CFRunLoopAddItemToCommonModes中跳到步驟5;
  5. 查看名為modename的mode是否在runloop對象中存在,不存在則創(chuàng)建,得到rlm對象;
  6. 將timer對象添加到rlm對象的timers集合中;
  7. 將rlm對象添加到runloop的_modes對象中。

CommonMode和DefaultMode的區(qū)別

這里要區(qū)分主線程和子線程,蘋果是區(qū)別處理的。

子線程的CommonMode和DefaultMode

子線程的情況比較簡單,在runloop對象的_modes中,只有KCFRunLoopDefaultMode,沒有名為"NSRunLoopCommonModes"的Mode。
通常我們會以下兩個(gè)方法來添加timer/source:

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

所有通過以上方法往NSRunLoopCommonModes中添加的源,首先會被添加在_commonModeItems的集合中,如流程圖中的步驟3。然后被加在_modes中KCFRunLoopDefaultMode的Mode中,如流程圖中的4和5。NSRunLoopCommonModes是一個(gè)虛擬的mode,它在_modes集合和整個(gè)runloop中對象都不存在該名稱的mode對象。但是需要注意的是直接往"KCFRunLoopDefaultMode"中添加的源不會填加到_commonModeItems的集合中。可以這么認(rèn)為,_commonModeItems中只包含往"NSRunLoopCommonModes"這個(gè)虛擬Mode中加的源。而_modes中名為KCFRunLoopDefaultMode的Mode是一個(gè)真實(shí)存在的mode,它是NSRunLoopCommonModes和KCFRunLoopDefaultMode源的集合。
子線程的runloop初始時(shí)只有一個(gè)KCFRunLoopDefaultMode。

主線程的CommonMode和DefaultMode

主線程和子線程類似。主線程runloop初始時(shí)_modes中有三個(gè)mode對象:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 、 UITrackingRunLoopMode和GSEventReceiveRunLoopMode。當(dāng)往主線程的NSRunLoopCommonModes中添加源時(shí)。也會先往_commonModeItems中添加,然后會分別往_modes中的UITrackingRunLoopMode和kCFRunLoopDefaultMode都添加一份。因?yàn)閁ITrackingRunLoopMode和kCFRunLoopDefaultMode支持NSRunLoopCommonModes。這也就是加在commonModes中的定時(shí)器無論是否滑動都會觸發(fā)。
但是奇怪的是主線程runloop對象的_modes中真實(shí)存在一個(gè)名為"KCFRunLoopCommonModes"的Mode對象。但是該對象中source,timer和observer都是空。

這里需要注意的是,即使把timer加在主線程的NSRunLoopCommonModes中也不一定保證timer一定會準(zhǔn)時(shí),因?yàn)橹骶€程還可能運(yùn)行在GSEventReceiveRunLoopMode中。甚至是我們自定義的mode,雖然這不常見。

外傳

這里有三個(gè)方法需要注意:

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

首先說明的是方法1沒有地方調(diào)用。應(yīng)該是蘋果并不想我們通過該方法往_commonMode中加內(nèi)容。該方法需要兩個(gè)參數(shù)rl即要操作的runloop,modename即要添加_commonMode中的字符串。這里就不能保證_commonMode中的名稱是唯一的(KCFRunLoopDefaultMode)。因?yàn)镸ode是用名字區(qū)分的。如果commonMode的名稱不是唯一的豈不亂了。
方法2只在方法1中調(diào)用,也就說明,方法2也是沒有地方調(diào)用的。其中valeu是要添加的源
即source/timer/observer。ctx是一個(gè)void的數(shù)組。ctx[0]是runloop對象,ctx[1]是mode name。
方法3是往commonMode中添加源的正常方法。value是mode name。 ctx也是是一個(gè)void
的數(shù)組,ctx[0]是runloop對象,ctx[1]是要添加的源,即source/timer/observer。注意該方法與方法2參數(shù)的區(qū)別。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容