iOS RunLoop詳解

Runloop 是和線程緊密相關(guān)的一個基礎(chǔ)組件,是很多線程有關(guān)功能的幕后功臣。盡管在平常使用中幾乎不太會直接用到,理解 Runloop 有利于我們更加深入地理解 iOS 的多線程模型。

本文從如下幾個方面理解RunLoop的相關(guān)知識點。

  • RunLoop概念
  • RunLoop實現(xiàn)
  • RunLoop運行
  • RunLoop應(yīng)用

RunLoop概念

RunLoop介紹

RunLoop 是什么?RunLoop 還是比較顧名思義的一個東西,說白了就是一種循環(huán),只不過它這種循環(huán)比較高級。一般的 while 循環(huán)會導(dǎo)致 CPU 進入忙等待狀態(tài),而 RunLoop 則是一種“閑”等待,這部分可以類比 Linux 下的 epoll。當(dāng)沒有事件時,RunLoop 會進入休眠狀態(tài),有事件發(fā)生時, RunLoop 會去找對應(yīng)的 Handler 處理事件。RunLoop 可以讓線程在需要做事的時候忙起來,不需要的話就讓線程休眠。

從代碼上看,RunLoop其實就是一個對象,它的結(jié)構(gòu)如下,源碼看這里

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* locked for accessing mode list */
    __CFPort _wakeUpPort;   // used for CFRunLoopWakeUp 內(nèi)核向該端口發(fā)送消息可以喚醒runloop
    Boolean _unused;
    volatile _per_run_data *_perRunData; // reset for runs of the run loop
    pthread_t _pthread;             //RunLoop對應(yīng)的線程
    uint32_t _winthread;
    CFMutableSetRef _commonModes;    //存儲的是字符串,記錄所有標記為common的mode
    CFMutableSetRef _commonModeItems;//存儲所有commonMode的item(source、timer、observer)
    CFRunLoopModeRef _currentMode;   //當(dāng)前運行的mode
    CFMutableSetRef _modes;          //存儲的是CFRunLoopModeRef
    struct _block_item *_blocks_head;//doblocks的時候用到
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};

可見,一個RunLoop對象,主要包含了一個線程,若干個Mode,若干個commonMode,還有一個當(dāng)前運行的Mode。

RunLoop與線程

當(dāng)我們需要一個常駐線程,可以讓線程在需要做事的時候忙起來,不需要的話就讓線程休眠。我們就在線程里面執(zhí)行下面這個代碼,一直等待消息,線程就不會退出了。

do {
   //獲取消息
   //處理消息
} while (消息 != 退出)

上面的這種循環(huán)模型被稱作 Event Loop,事件循環(huán)模型在眾多系統(tǒng)里都有實現(xiàn),RunLoop 實際上就是一個對象,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數(shù)來執(zhí)行上面 Event Loop 的邏輯。線程執(zhí)行了這個函數(shù)后,就會一直處于這個函數(shù)內(nèi)部 "接受消息->等待->處理" 的循環(huán)中,直到這個循環(huán)結(jié)束(比如傳入 quit 的消息),函數(shù)返回。

下圖描述了Runloop運行流程(基本描述了上面Runloop的核心流程,當(dāng)然可以查看官方The Run Loop Sequence of Events描述):

整個流程并不復(fù)雜(需要注意的就是黃色區(qū)域的消息處理中并不包含source0,因為它在循環(huán)開始之初就會處理),整個流程其實就是一種Event Loop的實現(xiàn),其他平臺均有類似的實現(xiàn),只是這里叫做RunLoop。

RunLoop與線程的關(guān)系如下圖

圖中展現(xiàn)了 Runloop 在線程中的作用:從 input source 和 timer source 接受事件,然后在線程中處理事件。

Runloop 和線程是綁定在一起的。每個線程(包括主線程)都有一個對應(yīng)的 Runloop 對象。我們并不能自己創(chuàng)建 Runloop 對象,但是可以獲取到系統(tǒng)提供的 Runloop 對象。

主線程的 Runloop 會在應(yīng)用啟動的時候完成啟動,其他線程的 Runloop 默認并不會啟動,需要我們手動啟動。

RunLoop Mode

Mode可以視為事件的管家,一個Mode管理著各種事件,它的結(jié)構(gòu)如下:

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;   //mode名稱
    Boolean _stopped;    //mode是否被終止
    char _padding[3];
    //幾種事件
    CFMutableSetRef _sources0;  //sources0
    CFMutableSetRef _sources1;  //sources1
    CFMutableArrayRef _observers; //通知
    CFMutableArrayRef _timers;    //定時器
    CFMutableDictionaryRef _portToV1SourceMap; //字典  key是mach_port_t,value是CFRunLoopSourceRef
    __CFPortSet _portSet; //保存所有需要監(jiān)聽的port,比如_wakeUpPort,_timerPort都保存在這個數(shù)組中
    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 */
};

一個CFRunLoopMode對象有一個name,若干source0、source1、timer、observer和若干port,可見事件都是由Mode在管理,而RunLoop管理Mode。

從源碼很容易看出,Runloop總是運行在某種特定的CFRunLoopModeRef下(每次運行__CFRunLoopRun()函數(shù)時必須指定Mode)。而通過CFRunloopRef對應(yīng)結(jié)構(gòu)體的定義可以很容易知道每種Runloop都可以包含若干個Mode,每個Mode又包含Source/Timer/Observer。每次調(diào)用Runloop的主函數(shù)__CFRunLoopRun()時必須指定一種Mode,這個Mode稱為 _currentMode,當(dāng)切換Mode時必須退出當(dāng)前Mode,然后重新進入Runloop以保證不同Mode的Source/Timer/Observer互不影響。

如圖所示,Runloop Mode 實際上是 Source,Timer 和 Observer 的集合,不同的 Mode 把不同組的 Source,Timer 和 Observer 隔絕開來。Runloop 在某個時刻只能跑在一個 Mode 下,處理這一個 Mode 當(dāng)中的 Source,Timer 和 Observer。

蘋果文檔中提到的 Mode 有五個,分別是:

  • NSDefaultRunLoopMode
  • NSConnectionReplyMode
  • NSModalPanelRunLoopMode
  • NSEventTrackingRunLoopMode
  • NSRunLoopCommonModes

iOS 中公開暴露出來的只有 NSDefaultRunLoopMode 和 NSRunLoopCommonModes。 NSRunLoopCommonModes 實際上是一個 Mode 的集合,默認包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode(注意:并不是說Runloop會運行在kCFRunLoopCommonModes這種模式下,而是相當(dāng)于分別注冊了 NSDefaultRunLoopMode和 UITrackingRunLoopMode。當(dāng)然你也可以通過調(diào)用CFRunLoopAddCommonMode()方法將自定義Mode放到 kCFRunLoopCommonModes組合)。

五種Mode的介紹如下圖:

RunLoop Source

Run Loop Source分為Source、Observer、Timer三種,他們統(tǒng)稱為ModeItem。

CFRunLoopSource

根據(jù)官方的描述,CFRunLoopSource是對input sources的抽象。CFRunLoopSource分source0和source1兩個版本,它的結(jié)構(gòu)如下:

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits; //用于標記Signaled狀態(tài),source0只有在被標記為Signaled狀態(tài),才會被處理
    pthread_mutex_t _lock;
    CFIndex _order;         /* immutable */
    CFMutableBagRef _runLoops;
    union {
        CFRunLoopSourceContext version0;     /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};

source0是App內(nèi)部事件,由App自己管理的UIEvent、CFSocket都是source0。當(dāng)一個source0事件準備執(zhí)行的時候,必須要先把它標記為signal狀態(tài),以下是source0的結(jié)構(gòu)體:

typedef struct {
    CFIndex version;
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
    void    (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*perform)(void *info);
} CFRunLoopSourceContext;

source0是非基于Port的。只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件。使用時,你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。

source1由RunLoop和內(nèi)核管理,source1帶有mach_port_t,可以接收內(nèi)核消息并觸發(fā)回調(diào),以下是source1的結(jié)構(gòu)體

typedef struct {
    CFIndex version;
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
    mach_port_t (*getPort)(void *info);
    void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
    void *  (*getPort)(void *info);
    void    (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;

Source1除了包含回調(diào)指針外包含一個mach port,Source1可以監(jiān)聽系統(tǒng)端口和通過內(nèi)核和其他線程通信,接收、分發(fā)系統(tǒng)事件,它能夠主動喚醒RunLoop(由操作系統(tǒng)內(nèi)核進行管理,例如CFMessagePort消息)。官方也指出可以自定義Source,因此對于CFRunLoopSourceRef來說它更像一種協(xié)議,框架已經(jīng)默認定義了兩種實現(xiàn),如果有必要開發(fā)人員也可以自定義,詳細情況可以查看官方文檔。

CFRunLoopObserver

CFRunLoopObserver是觀察者,可以觀察RunLoop的各種狀態(tài),并拋出回調(diào)。

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;      /* immutable */
    CFIndex _order;         /* immutable */
    CFRunLoopObserverCallBack _callout; /* immutable */
    CFRunLoopObserverContext _context;  /* immutable, except invalidation */
};

CFRunLoopObserver可以觀察的狀態(tài)有如下6種:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), //即將進入run loop
    kCFRunLoopBeforeTimers = (1UL << 1), //即將處理timer
    kCFRunLoopBeforeSources = (1UL << 2),//即將處理source
    kCFRunLoopBeforeWaiting = (1UL << 5),//即將進入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),//被喚醒但是還沒開始處理事件
    kCFRunLoopExit = (1UL << 7),//run loop已經(jīng)退出
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

Runloop 通過監(jiān)控 Source 來決定有沒有任務(wù)要做,除此之外,我們還可以用 Runloop Observer 來監(jiān)控 Runloop 本身的狀態(tài)。 Runloop Observer 可以監(jiān)控上面的 Runloop 事件,具體流程如下圖。

CFRunLoopTimer

CFRunLoopTimer是定時器,可以在設(shè)定的時間點拋出回調(diào),它的結(jié)構(gòu)如下:

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;  //標記fire狀態(tài)
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;        //添加該timer的runloop
    CFMutableSetRef _rlModes;     //存放所有 包含該timer的 mode的 modeName,意味著一個timer可能會在多個mode中存在
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;     //理想時間間隔  /* immutable */
    CFTimeInterval _tolerance;    //時間偏差      /* mutable */
    uint64_t _fireTSR;          /* TSR units */
    CFIndex _order;         /* immutable */
    CFRunLoopTimerCallBack _callout;    /* immutable */
    CFRunLoopTimerContext _context; /* immutable, except invalidation */
};

另外根據(jù)官方文檔的描述,CFRunLoopTimer和NSTimer是toll-free bridged的,可以相互轉(zhuǎn)換。

CFRunLoopTimer is “toll-free bridged” with its Cocoa Foundation counterpart, NSTimer. This means that the Core Foundation type is interchangeable in function or method calls with the bridged Foundation object.

所以CFRunLoopTimer具有以下特性:

  • CFRunLoopTimer 是定時器,可以在設(shè)定的時間點拋出回調(diào)
  • CFRunLoopTimer和NSTimer是toll-free bridged的,可以相互轉(zhuǎn)換

RunLoop實現(xiàn)

下面從以下3個方面介紹RunLoop的實現(xiàn)。

  • 獲取RunLoop
  • 添加Mode
  • 添加Run Loop Source

獲取RunLoop

從蘋果開放的API來看,不允許我們直接創(chuàng)建RunLoop對象,只能通過以下幾個函數(shù)來獲取RunLoop:

  • CFRunLoopRef CFRunLoopGetCurrent(void)
  • CFRunLoopRef CFRunLoopGetMain(void)
  • +(NSRunLoop *)currentRunLoop
  • +(NSRunLoop *)mainRunLoop

前兩個是Core Foundation中的API,后兩個是Foundation中的API。

那么RunLoop是什么時候被創(chuàng)建的呢?

我們從下面幾個函數(shù)內(nèi)部看看。

CFRunLoopGetCurrent
//取當(dāng)前所在線程的RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    //傳入當(dāng)前線程
    return _CFRunLoopGet0(pthread_self());
}

在CFRunLoopGetCurrent函數(shù)內(nèi)部調(diào)用了_CFRunLoopGet0(),傳入的參數(shù)是當(dāng)前線程pthread_self()。這里可以看出,CFRunLoopGetCurrent函數(shù)必須要在線程內(nèi)部調(diào)用,才能獲取當(dāng)前線程的RunLoop。也就是說子線程的RunLoop必須要在子線程內(nèi)部獲取。

CFRunLoopGetMain
//取主線程的RunLoop
CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    //傳入主線程
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

在CFRunLoopGetMain函數(shù)內(nèi)部也調(diào)用了_CFRunLoopGet0(),傳入的參數(shù)是主線程pthread_main_thread_np()??梢钥闯?,CFRunLoopGetMain()不管在主線程還是子線程中調(diào)用,都可以獲取到主線程的RunLoop。

CFRunLoopGet0

前面兩個函數(shù)都是使用了CFRunLoopGet0實現(xiàn)傳入線程的函數(shù),下面看下CFRunLoopGet0的結(jié)構(gòu)是咋樣的。

static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFSpinLock_t loopsLock = CFSpinLockInit;
 
// t==0 is a synonym for "main thread" that always works
//根據(jù)線程取RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    __CFSpinLock(&loopsLock);
    //如果存儲RunLoop的字典不存在
    if (!__CFRunLoops) {
        __CFSpinUnlock(&loopsLock);
        //創(chuàng)建一個臨時字典dict
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //創(chuàng)建主線程的RunLoop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        //把主線程的RunLoop保存到dict中,key是線程,value是RunLoop
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        //此處NULL和__CFRunLoops指針都指向NULL,匹配,所以將dict寫到__CFRunLoops
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            //釋放dict
            CFRelease(dict);
        }
        //釋放mainrunloop
        CFRelease(mainLoop);
        __CFSpinLock(&loopsLock);
    }
    //以上說明,第一次進來的時候,不管是getMainRunloop還是get子線程的runloop,主線程的runloop總是會被創(chuàng)建
    //從字典__CFRunLoops中獲取傳入線程t的runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);
    //如果沒有獲取到
    if (!loop) {
        //根據(jù)線程t創(chuàng)建一個runloop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            //把newLoop存入字典__CFRunLoops,key是線程t
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFSpinUnlock(&loopsLock);
        CFRelease(newLoop);
    }
    //如果傳入線程就是當(dāng)前線程
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            //注冊一個回調(diào),當(dāng)線程銷毀時,銷毀對應(yīng)的RunLoop
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

這段代碼可以得出以下結(jié)論:

  • RunLoop和線程的一一對應(yīng)的,對應(yīng)的方式是以key-value的方式保存在一個全局字典中
  • 主線程的RunLoop會在初始化全局字典時創(chuàng)建
  • 子線程的RunLoop會在第一次獲取的時候創(chuàng)建,如果不獲取的話就一直不會被創(chuàng)建
  • RunLoop會在線程銷毀時銷毀

添加Mode

在Core Foundation中,針對Mode的操作,蘋果只開放了以下3個API(Cocoa中也有功能一樣的函數(shù),不再列出):

  • CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode)
  • CFStringRef CFRunLoopCopyCurrentMode(CFRunLoopRef rl)
  • CFArrayRef CFRunLoopCopyAllModes(CFRunLoopRef rl)

CFRunLoopAddCommonMode
Adds a mode to the set of run loop common modes.
向當(dāng)前RunLoop的common modes中添加一個mode。

CFRunLoopCopyCurrentMode
Returns the name of the mode in which a given run loop is currently running.
返回當(dāng)前運行的mode的name

CFRunLoopCopyAllModes
Returns an array that contains all the defined modes for a CFRunLoop object.
返回當(dāng)前RunLoop的所有mode

我們沒有辦法直接創(chuàng)建一個CFRunLoopMode對象,但是我們可以調(diào)用CFRunLoopAddCommonMode傳入一個字符串向RunLoop中添加Mode,傳入的字符串即為Mode的名字,Mode對象應(yīng)該是此時在RunLoop內(nèi)部創(chuàng)建的。下面來看一下源碼。

CFRunLoopAddCommonMode
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    __CFRunLoopLock(rl);
    //看rl中是否已經(jīng)有這個mode,如果有就什么都不做
    if (!CFSetContainsValue(rl->_commonModes, modeName)) {
        CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
        //把modeName添加到RunLoop的_commonModes中
        CFSetAddValue(rl->_commonModes, modeName);
        if (NULL != set) {
            CFTypeRef context[2] = {rl, modeName};
            /* add all common-modes items to new mode */
            //這里調(diào)用CFRunLoopAddSource/CFRunLoopAddObserver/CFRunLoopAddTimer的時候會調(diào)用
            //__CFRunLoopFindMode(rl, modeName, true),CFRunLoopMode對象在這個時候被創(chuàng)建
            CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
            CFRelease(set);
        }
    } else {
    }
    __CFRunLoopUnlock(rl);
}

可以看得出:

  • modeName不能重復(fù),modeName是mode的唯一標識符
  • RunLoop的_commonModes數(shù)組存放所有被標記為common的mode的名稱
  • 添加commonMode會把commonModeItems數(shù)組中的所有source同步到新添加的mode中
  • CFRunLoopMode對象在CFRunLoopAddItemsToCommonMode函數(shù)中調(diào)用CFRunLoopFindMode時被創(chuàng)建
CFRunLoopCopyCurrentMode/CFRunLoopCopyAllModes

CFRunLoopCopyCurrentMode和CFRunLoopCopyAllModes的內(nèi)部邏輯比較簡單,直接取RunLoop的_currentMode和_modes返回,就不貼源碼了。

添加Run Loop Source(ModeItem)

我們可以通過以下接口添加/移除各種事件:

  • void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
  • void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
  • void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
  • void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef * mode)
  • void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
  • void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
CFRunLoopAddSource

CFRunLoopAddSource的代碼結(jié)構(gòu)如下:

//添加source事件
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {    /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    if (!__CFIsValid(rls)) return;
    Boolean doVer0Callout = false;
    __CFRunLoopLock(rl);
    //如果是kCFRunLoopCommonModes
    if (modeName == kCFRunLoopCommonModes) {
        //如果runloop的_commonModes存在,則copy一個新的復(fù)制給set
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
       //如果runl _commonModeItems為空
        if (NULL == rl->_commonModeItems) {
            //先初始化
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        //把傳入的CFRunLoopSourceRef加入_commonModeItems
        CFSetAddValue(rl->_commonModeItems, rls);
        //如果剛才set copy到的數(shù)組里有數(shù)據(jù)
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rls};
            /* add new item to all common-modes */
            //則把set里的所有mode都執(zhí)行一遍__CFRunLoopAddItemToCommonModes函數(shù)
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
        //以上分支的邏輯就是,如果你往kCFRunLoopCommonModes里面添加一個source,那么所有_commonModes里的mode都會添加這個source
    } else {
        //根據(jù)modeName查找mode
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
        //如果_sources0不存在,則初始化_sources0,_sources0和_portToV1SourceMap
        if (NULL != rlm && NULL == rlm->_sources0) {
            rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
        }
        //如果_sources0和_sources1中都不包含傳入的source
        if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
            //如果version是0,則加到_sources0
            if (0 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources0, rls);
                //如果version是1,則加到_sources1
            } else if (1 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources1, rls);
                __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
                if (CFPORT_NULL != src_port) {
                    //此處只有在加到source1的時候才會把souce和一個mach_port_t對應(yīng)起來
                    //可以理解為,source1可以通過內(nèi)核向其端口發(fā)送消息來主動喚醒runloop
                    CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
                    __CFPortSetInsert(src_port, rlm->_portSet);
                }
            }
            __CFRunLoopSourceLock(rls);
            //把runloop加入到source的_runLoops中
            if (NULL == rls->_runLoops) {
                rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!
            }
            CFBagAddValue(rls->_runLoops, rl);
            __CFRunLoopSourceUnlock(rls);
            if (0 == rls->_context.version0.version) {
                if (NULL != rls->_context.version0.schedule) {
                    doVer0Callout = true;
                }
            }
        }
        if (NULL != rlm) {
            __CFRunLoopModeUnlock(rlm);
        }
    }
    __CFRunLoopUnlock(rl);
    if (doVer0Callout) {
        // although it looses some protection for the source, we have no choice but
        // to do this after unlocking the run loop and mode locks, to avoid deadlocks
        // where the source wants to take a lock which is already held in another
        // thread which is itself waiting for a run loop/mode lock
        rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */
    }
}

通過添加source的這段代碼可以得出如下結(jié)論:

  • 如果modeName傳入kCFRunLoopCommonModes,則該source會被保存到RunLoop的_commonModeItems中
  • 如果modeName傳入kCFRunLoopCommonModes,則該source會被添加到所有commonMode中
  • 如果modeName傳入的不是kCFRunLoopCommonModes,則會先查找該Mode,如果沒有,會創(chuàng)建一個
  • 同一個source在一個mode中只能被添加一次
CFRunLoopRemoveSource

remove操作和add操作的邏輯基本一致,很容易理解。

//移除source
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */
    CHECK_FOR_FORK();
    Boolean doVer0Callout = false, doRLSRelease = false;
    __CFRunLoopLock(rl);
    //如果是kCFRunLoopCommonModes,則從_commonModes的所有mode中移除該source
    if (modeName == kCFRunLoopCommonModes) {
        if (NULL != rl->_commonModeItems && CFSetContainsValue(rl->_commonModeItems, rls)) {
            CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
            CFSetRemoveValue(rl->_commonModeItems, rls);
            if (NULL != set) {
                CFTypeRef context[2] = {rl, rls};
                /* remove new item from all common-modes */
                CFSetApplyFunction(set, (__CFRunLoopRemoveItemFromCommonModes), (void *)context);
                CFRelease(set);
            }
        } else {
        }
    } else {
        //根據(jù)modeName查找mode,如果不存在,返回NULL
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false);
        if (NULL != rlm && ((NULL != rlm->_sources0 && CFSetContainsValue(rlm->_sources0, rls)) || (NULL != rlm->_sources1 && CFSetContainsValue(rlm->_sources1, rls)))) {
            CFRetain(rls);
            //根據(jù)source版本做對應(yīng)的remove操作
            if (1 == rls->_context.version0.version) {
                __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
                if (CFPORT_NULL != src_port) {
                    CFDictionaryRemoveValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port);
                    __CFPortSetRemove(src_port, rlm->_portSet);
                }
            }
            CFSetRemoveValue(rlm->_sources0, rls);
            CFSetRemoveValue(rlm->_sources1, rls);
            __CFRunLoopSourceLock(rls);
            if (NULL != rls->_runLoops) {
                CFBagRemoveValue(rls->_runLoops, rl);
            }
            __CFRunLoopSourceUnlock(rls);
            if (0 == rls->_context.version0.version) {
                if (NULL != rls->_context.version0.cancel) {
                    doVer0Callout = true;
                }
            }
            doRLSRelease = true;
        }
        if (NULL != rlm) {
            __CFRunLoopModeUnlock(rlm);
        }
    }
    __CFRunLoopUnlock(rl);
    if (doVer0Callout) {
        // although it looses some protection for the source, we have no choice but
        // to do this after unlocking the run loop and mode locks, to avoid deadlocks
        // where the source wants to take a lock which is already held in another
        // thread which is itself waiting for a run loop/mode lock
        rls->_context.version0.cancel(rls->_context.version0.info, rl, modeName);   /* CALLOUT */
    }
    if (doRLSRelease) CFRelease(rls);
}
添加Observer和Timer

添加observer和timer的內(nèi)部邏輯和添加source大體類似。

區(qū)別在于observer和timer只能被添加到一個RunLoop的一個或者多個mode中,比如一個timer被添加到主線程的RunLoop中,則不能再把該timer添加到子線程的RunLoop,而source沒有這個限制,不管是哪個RunLoop,只要mode中沒有,就可以添加。

這個區(qū)別在文章最開始的結(jié)構(gòu)體中也可以發(fā)現(xiàn),CFRunLoopSource結(jié)構(gòu)體中有保存RunLoop對象的數(shù)組,而CFRunLoopObserver和CFRunLoopTimer只有單個RunLoop對象。

RunLoop運行

在Core Foundation中我們可以通過以下2個API來讓RunLoop運行:

  • void CFRunLoopRun(void)

在默認的mode下運行當(dāng)前線程的RunLoop。

  • CFRunLoopRunResult CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled)

在指定mode下運行當(dāng)前線程的RunLoop。

CFRunLoopRun

//默認運行runloop的kCFRunLoopDefaultMode
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        //默認在kCFRunLoopDefaultMode下運行runloop
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

在CFRunLoopRun函數(shù)中調(diào)用了CFRunLoopRunSpecific函數(shù),runloop參數(shù)傳入當(dāng)前RunLoop對象,modeName參數(shù)傳入kCFRunLoopDefaultMode。驗證了前面文檔的解釋。

CFRunLoopRunInMode

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

在CFRunLoopRunInMode函數(shù)中也調(diào)用了CFRunLoopRunSpecific函數(shù),runloop參數(shù)傳入當(dāng)前RunLoop對象,modeName參數(shù)繼續(xù)傳遞CFRunLoopRunInMode傳入的modeName。也驗證了前面文檔的解釋。

這里還可以看出,雖然RunLoop有很多個mode,但是RunLoop在run的時候必須只能指定其中一個mode,運行起來之后,被指定的mode即為currentMode。

這2個函數(shù)都看不出來RunLoop是怎么run起來的。

接下來我們繼續(xù)探索一下CFRunLoopRunSpecific函數(shù)里面都干了什么,看看RunLoop具體是怎么run的。

CFRunLoopRunSpecific

/*
 * 指定mode運行runloop
 * @param rl 當(dāng)前運行的runloop
 * @param modeName 需要運行的mode的name
 * @param seconds  runloop的超時時間
 * @param returnAfterSourceHandled 是否處理完事件就返回
 */
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    //根據(jù)modeName找到本次運行的mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    //如果沒找到 || mode中沒有注冊任何事件,則就此停止,不進入循環(huán)
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        if (currentMode) __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopUnlock(rl);
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    //取上一次運行的mode
    CFRunLoopModeRef previousMode = rl->_currentMode;
    //如果本次mode和上次的mode一致
    rl->_currentMode = currentMode;
    //初始化一個result為kCFRunLoopRunFinished
    int32_t result = kCFRunLoopRunFinished;
    
    // 1.通知observer即將進入runloop
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //10.通知observer已退出runloop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

通過CFRunLoopRunSpecific的內(nèi)部邏輯,我們可以得出:

  • 如果指定了一個不存在的mode來運行RunLoop,那么會失敗,mode不會被創(chuàng)建,所以這里傳入的mode必須是存在的
  • 如果指定了一個mode,但是這個mode中不包含任何modeItem,那么RunLoop也不會運行,所以必須要* 傳入至少包含一個modeItem的mode
  • 在進入run loop之前通知observer,狀態(tài)為kCFRunLoopEntry
  • 在退出run loop之后通知observer,狀態(tài)為kCFRunLoopExit

RunLoop的運行的最核心函數(shù)是__CFRunLoopRun,接下來我們分析__CFRunLoopRun的源碼。

__CFRunLoopRun

這段代碼比較長,請做好心理準備,我已經(jīng)加了比較詳細的注釋。本節(jié)開頭的run loop運行步驟2~9步都在下面的代碼中得到驗證。

/**
 *  運行run loop
 *
 *  @param rl              運行的RunLoop對象
 *  @param rlm             運行的mode
 *  @param seconds         run loop超時時間
 *  @param stopAfterHandle true:run loop處理完事件就退出  false:一直運行直到超時或者被手動終止
 *  @param previousMode    上一次運行的mode
 *
 *  @return 返回4種狀態(tài)
 */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    //獲取系統(tǒng)啟動后的CPU運行時間,用于控制超時時間
    uint64_t startTSR = mach_absolute_time();
    
    //如果RunLoop或者mode是stop狀態(tài),則直接return,不進入循環(huán)
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
        return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;
        return kCFRunLoopRunStopped;
    }
    
    //mach端口,在內(nèi)核中,消息在端口之間傳遞。 初始為0
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    //判斷是否為主線程
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
    //如果在主線程 && runloop是主線程的runloop && 該mode是commonMode,則給mach端口賦值為主線程收發(fā)消息的端口
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
    
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    mach_port_name_t modeQueuePort = MACH_PORT_NULL;
    if (rlm->_queue) {
        //mode賦值為dispatch端口_dispatch_runloop_root_queue_perform_4CF
        modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
        if (!modeQueuePort) {
            CRASH("Unable to get port for run loop mode queue (%d)", -1);
        }
    }
#endif
    
    //GCD管理的定時器,用于實現(xiàn)runloop超時機制
    dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    
    //立即超時
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    }
    //seconds為超時時間,超時時執(zhí)行__CFRunLoopTimeout函數(shù)
    else if (seconds <= TIMER_INTERVAL_LIMIT) {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
        timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_retain(timeout_timer);
        timeout_context->ds = timeout_timer;
        timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
        timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
        dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
        dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
        uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
        dispatch_resume(timeout_timer);
    }
    //永不超時
    else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }
    
    //標志位默認為true
    Boolean didDispatchPortLastTime = true;
    //記錄最后runloop狀態(tài),用于return
    int32_t retVal = 0;
    do {
        //初始化一個存放內(nèi)核消息的緩沖池
        uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        mach_msg_header_t *msg = NULL;
        mach_port_t livePort = MACH_PORT_NULL;
#elif DEPLOYMENT_TARGET_WINDOWS
        HANDLE livePort = NULL;
        Boolean windowsMessageReceived = false;
#endif
        //取所有需要監(jiān)聽的port
        __CFPortSet waitSet = rlm->_portSet;
        
        //設(shè)置RunLoop為可以被喚醒狀態(tài)
        __CFRunLoopUnsetIgnoreWakeUps(rl);
        
        //2.通知observer,即將觸發(fā)timer回調(diào),處理timer事件
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        //3.通知observer,即將觸發(fā)Source0回調(diào)
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
        //執(zhí)行加入當(dāng)前runloop的block
        __CFRunLoopDoBlocks(rl, rlm);
        
        //4.處理source0事件
        //有事件處理返回true,沒有事件返回false
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            //執(zhí)行加入當(dāng)前runloop的block
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        //如果沒有Sources0事件處理 并且 沒有超時,poll為false
        //如果有Sources0事件處理 或者 超時,poll都為true
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
        //第一次do..whil循環(huán)不會走該分支,因為didDispatchPortLastTime初始化是true
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            //從緩沖區(qū)讀取消息
            msg = (mach_msg_header_t *)msg_buffer;
            //5.接收dispatchPort端口的消息,(接收source1事件)
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
                //如果接收到了消息的話,前往第9步開始處理msg
                goto handle_msg;
            }
#elif DEPLOYMENT_TARGET_WINDOWS
            if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
                goto handle_msg;
            }
#endif
        }
        
        didDispatchPortLastTime = false;
        
        //6.通知觀察者RunLoop即將進入休眠
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        //設(shè)置RunLoop為休眠狀態(tài)
        __CFRunLoopSetSleeping(rl);
        // do not do any user callouts after this point (after notifying of sleeping)
        
        // Must push the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced.
        
        __CFPortSetInsert(dispatchPort, waitSet);
        
        __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //這里有個內(nèi)循環(huán),用于接收等待端口的消息
        //進入此循環(huán)后,線程進入休眠,直到收到新消息才跳出該循環(huán),繼續(xù)執(zhí)行run loop
        do {
            if (kCFUseCollectableAllocator) {
                objc_clear_stack(0);
                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;
            //7.接收waitSet端口的消息
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
            //收到消息之后,livePort的值為msg->msgh_local_port,
            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                if (rlm->_timerFired) {
                    // Leave livePort as the queue port, and service timers below
                    rlm->_timerFired = false;
                    break;
                } else {
                    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                }
            } else {
                // Go ahead and leave the inner loop.
                break;
            }
        } while (1);
#else
        if (kCFUseCollectableAllocator) {
            objc_clear_stack(0);
            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
#endif
        
        
#elif DEPLOYMENT_TARGET_WINDOWS
        // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
        __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif
        
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
        
        // Must remove the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced. Also, we don't want them left
        // in there if this function returns.
        
        __CFPortSetRemove(dispatchPort, waitSet);
        
 
        __CFRunLoopSetIgnoreWakeUps(rl);
        
        // user callouts now OK again
        //取消runloop的休眠狀態(tài)
        __CFRunLoopUnsetSleeping(rl);
        //8.通知觀察者runloop被喚醒
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
      
        //9.處理收到的消息
    handle_msg:;
        __CFRunLoopSetIgnoreWakeUps(rl);
        
#if DEPLOYMENT_TARGET_WINDOWS
        if (windowsMessageReceived) {
            // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            
            if (rlm->_msgPump) {
                rlm->_msgPump();
            } else {
                MSG msg;
                if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                }
            }
            
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            
            // To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
            // Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
            // NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
            __CFRunLoopSetSleeping(rl);
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            
            __CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);
            
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            __CFRunLoopUnsetSleeping(rl);
            // If we have a new live port then it will be handled below as normal
        }
        
        
#endif
        if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
            //通過CFRunloopWake喚醒
        } else if (livePort == rl->_wakeUpPort) {
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();
            //什么都不干,跳回2重新循環(huán)
            // do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
            // Always reset the wake up port, or risk spinning forever
            ResetEvent(rl->_wakeUpPort);
#endif
        }
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //如果是定時器事件
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            //9.1 處理timer事件
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer, because we apparently fired early
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
#if USE_MK_TIMER_TOO
        //如果是定時器事件
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
            // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
           //9.1處理timer事件
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
        //如果是dispatch到main queue的block
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
            void *msg = 0;
#endif
            //9.2執(zhí)行block
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
            // Despite the name, this works for windows handles as well
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            // 有source1事件待處理
            if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
                mach_msg_header_t *reply = NULL;
                //9.2 處理source1事件
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
                if (NULL != reply) {
                    (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
                    CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
                }
#elif DEPLOYMENT_TARGET_WINDOWS
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
            }
        }
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
        
        __CFRunLoopDoBlocks(rl, rlm);
        
        if (sourceHandledThisLoop && stopAfterHandle) {
            //進入run loop時傳入的參數(shù),處理完事件就返回
            retVal = kCFRunLoopRunHandledSource;
        }else if (timeout_context->termTSR < mach_absolute_time()) {
            //run loop超時
            retVal = kCFRunLoopRunTimedOut;
        }else if (__CFRunLoopIsStopped(rl)) {
            //run loop被手動終止
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        }else if (rlm->_stopped) {
            //mode被終止
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        }else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            //mode中沒有要處理的事件
            retVal = kCFRunLoopRunFinished;
        }
        //除了上面這幾種情況,都繼續(xù)循環(huán)
    } while (0 == retVal);
    
    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }
    
    return retVal;
}

__CFRunLoopServiceMachPort

第7步調(diào)用了__CFRunLoopServiceMachPort函數(shù),這個函數(shù)在run loop中起到了至關(guān)重要的作用,下面給出了詳細注釋。

/**
 *  接收指定內(nèi)核端口的消息
 *
 *  @param port        接收消息的端口
 *  @param buffer      消息緩沖區(qū)
 *  @param buffer_size 消息緩沖區(qū)大小
 *  @param livePort    暫且理解為活動的端口,接收消息成功時候值為msg->msgh_local_port,超時時為MACH_PORT_NULL
 *  @param timeout     超時時間,單位是ms,如果超時,則RunLoop進入休眠狀態(tài)
 *
 *  @return 接收消息成功時返回true 其他情況返回false
 */
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout) {
    Boolean originalBuffer = true;
    kern_return_t ret = KERN_SUCCESS;
    for (;;) {      /* In that sleep of death what nightmares may come ... */
        mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
        msg->msgh_bits = 0;  //消息頭的標志位
        msg->msgh_local_port = port;  //源(發(fā)出的消息)或者目標(接收的消息)
        msg->msgh_remote_port = MACH_PORT_NULL; //目標(發(fā)出的消息)或者源(接收的消息)
        msg->msgh_size = buffer_size;  //消息緩沖區(qū)大小,單位是字節(jié)
        msg->msgh_id = 0;  //唯一id
       
        if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
        
        //通過mach_msg發(fā)送或者接收的消息都是指針,
        //如果直接發(fā)送或者接收消息體,會頻繁進行內(nèi)存復(fù)制,損耗性能
        //所以XNU使用了單一內(nèi)核的方式來解決該問題,所有內(nèi)核組件都共享同一個地址空間,因此傳遞消息時候只需要傳遞消息的指針
        ret = mach_msg(msg,
                       MACH_RCV_MSG|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV),
                       0,
                       msg->msgh_size,
                       port,
                       timeout,
                       MACH_PORT_NULL);
        CFRUNLOOP_WAKEUP(ret);
        
        //接收/發(fā)送消息成功,給livePort賦值為msgh_local_port
        if (MACH_MSG_SUCCESS == ret) {
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            return true;
        }
        
        //MACH_RCV_TIMEOUT
        //超出timeout時間沒有收到消息,返回MACH_RCV_TIMED_OUT
        //此時釋放緩沖區(qū),把livePort賦值為MACH_PORT_NULL
        if (MACH_RCV_TIMED_OUT == ret) {
            if (!originalBuffer) free(msg);
            *buffer = NULL;
            *livePort = MACH_PORT_NULL;
            return false;
        }
        
        //MACH_RCV_LARGE
        //如果接收緩沖區(qū)太小,則將過大的消息放在隊列中,并且出錯返回MACH_RCV_TOO_LARGE,
        //這種情況下,只返回消息頭,調(diào)用者可以分配更多的內(nèi)存
        if (MACH_RCV_TOO_LARGE != ret) break;
        //此處給buffer分配更大內(nèi)存
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
        if (originalBuffer) *buffer = NULL;
        originalBuffer = false;
        *buffer = realloc(*buffer, buffer_size);
    }
    HALT;
    return false;
}

小結(jié)

RunLoop實際很簡單,它是一個對象,它和線程是一一對應(yīng)的,每個線程都有一個對應(yīng)的RunLoop對象,主線程的RunLoop會在程序啟動時自動創(chuàng)建,子線程需要手動獲取來創(chuàng)建。

RunLoop運行的核心是一個do..while..循環(huán),遍歷所有需要處理的事件,如果有事件處理就讓線程工作,沒有事件處理則讓線程休眠,同時等待事件到來。

RunLoop應(yīng)用

在開發(fā)過程中幾乎所有的操作都是通過Call out進行回調(diào)的(無論是Observer的狀態(tài)通知還是Timer、Source的處理),而系統(tǒng)在回調(diào)時通常使用如下幾個函數(shù)進行回調(diào)(換句話說你的代碼其實最終都是通過下面幾個函數(shù)來負責(zé)調(diào)用的,即使你自己監(jiān)聽Observer也會先調(diào)用下面的函數(shù)然后間接通知你,所以在調(diào)用堆棧中經(jīng)??吹竭@些函數(shù)):

    static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
    static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

實際的代碼塊如下:

{
    /// 1. 通知Observers,即將進入RunLoop
    /// 此處有Observer會創(chuàng)建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
 
        /// 2. 通知 Observers: 即將觸發(fā) Timer 回調(diào)。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即將觸發(fā) Source (非基于port的,Source0) 回調(diào)。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 4. 觸發(fā) Source0 (非基于port的) 回調(diào)。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 6. 通知Observers,即將進入休眠
        /// 此處有Observer釋放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
 
        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();
        
 
        /// 8. 通知Observers,線程被喚醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
 
        /// 9. 如果是被Timer喚醒的,回調(diào)Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
 
        /// 9. 如果是被dispatch喚醒的,執(zhí)行所有調(diào)用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
 
        /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件喚醒了,處理這個事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
 
 
    } while (...);
 
    /// 10. 通知Observers,即將退出RunLoop
    /// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

例如在控制器的touchBegin中打入斷點查看堆棧(由于UIEvent是Source0,所以可以看到一個Source0的Call out函數(shù)CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION調(diào)用):

NSTimer 與 GCD Timer、CADisplayLink

NSTimer

前面一直提到Timer Source作為事件源,事實上它的上層對應(yīng)就是NSTimer(其實就是CFRunloopTimerRef)這個開發(fā)者經(jīng)常用到的定時器(底層基于使用mk_timer實現(xiàn))

NSTimer 其實就是 CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個 NSTimer 注冊到 RunLoop 后,RunLoop 會為其重復(fù)的時間點注冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop為了節(jié)省資源,并不會在非常準確的時間點回調(diào)這個Timer。Timer 有個屬性叫做 Tolerance (寬容度),標示了當(dāng)時間點到后,容許有多少最大誤差。由于 NSTimer 的這種機制,因此 NSTimer 的執(zhí)行必須依賴于 RunLoop,如果沒有 RunLoop,NSTimer 是不會執(zhí)行的。

如果某個時間點被錯過了,例如執(zhí)行了一個很長的任務(wù),則那個時間點的回調(diào)也會跳過去,不會延后執(zhí)行。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。

GCD Timer

GCD 則不同,GCD 的線程管理是通過系統(tǒng)來直接管理的。GCD Timer 是通過 dispatch port 給 RunLoop 發(fā)送消息,來使 RunLoop 執(zhí)行相應(yīng)的 block,如果所在線程沒有 RunLoop,那么 GCD 會臨時創(chuàng)建一個線程去執(zhí)行 block,執(zhí)行完之后再銷毀掉,因此 GCD 的 Timer 是不依賴 RunLoop 的。

至于這兩個 Timer 的準確性問題,如果不在 RunLoop 的線程里面執(zhí)行,那么只能使用 GCD Timer,由于 GCD Timer 是基于 MKTimer(mach kernel timer),已經(jīng)很底層了,因此是很準確的。

如果在 RunLoop 的線程里面執(zhí)行,由于 GCD Timer 和 NSTimer 都是通過 port 發(fā)送消息的機制來觸發(fā) RunLoop 的,因此準確性差別應(yīng)該不是很大。如果線程 RunLoop 阻塞了,不管是 GCD Timer 還是 NSTimer 都會存在延遲問題。

CADisplayLink

CADisplayLink是一個執(zhí)行頻率(fps)和屏幕刷新相同(可以修改preferredFramesPerSecond改變刷新頻率)的定時器,它也需要加入到RunLoop才能執(zhí)行。與NSTimer類似,CADisplayLink同樣是基于CFRunloopTimerRef實現(xiàn),底層使用mk_timer(可以比較加入到RunLoop前后RunLoop中timer的變化)。和NSTimer相比它精度更高(盡管NSTimer也可以修改精度),不過和NStimer類似的是如果遇到大任務(wù)它仍然存在丟幀現(xiàn)象。通常情況下CADisaplayLink用于構(gòu)建幀動畫,看起來相對更加流暢,而NSTimer則有更廣泛的用處。

AutoreleasePool

AutoreleasePool是另一個與RunLoop相關(guān)討論較多的話題。其實從RunLoop源代碼分析,AutoreleasePool與RunLoop并沒有直接的關(guān)系,之所以將兩個話題放到一起討論最主要的原因是因為在iOS應(yīng)用啟動后會注冊兩個Observer管理和維護AutoreleasePool。不妨在應(yīng)用程序剛剛啟動時打印currentRunLoop可以看到系統(tǒng)默認注冊了很多個Observer,其中有兩個Observer的callout都是** _ wrapRunLoopWithAutoreleasePoolHandler**,這兩個是和自動釋放池相關(guān)的兩個監(jiān)聽。

<CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
<CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}

第一個Observer會監(jiān)聽RunLoop的進入,它會回調(diào)objc_autoreleasePoolPush()向當(dāng)前的AutoreleasePoolPage增加一個哨兵對象標志創(chuàng)建自動釋放池。這個Observer的order是-2147483647優(yōu)先級最高,確保發(fā)生在所有回調(diào)操作之前。
第二個Observer會監(jiān)聽RunLoop的進入休眠和即將退出RunLoop兩種狀態(tài),在即將進入休眠時會調(diào)用objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根據(jù)情況從最新加入的對象一直往前清理直到遇到哨兵對象。而在即將退出RunLoop時會調(diào)用objc_autoreleasePoolPop() 釋放自動自動釋放池內(nèi)對象。這個Observer的order是2147483647,優(yōu)先級最低,確保發(fā)生在所有回調(diào)操作之后。
主線程的其他操作通常均在這個AutoreleasePool之內(nèi)(main函數(shù)中),以盡可能減少內(nèi)存維護操作(當(dāng)然你如果需要顯式釋放【例如循環(huán)】時可以自己創(chuàng)建AutoreleasePool否則一般不需要自己創(chuàng)建)。
其實在應(yīng)用程序啟動后系統(tǒng)還注冊了其他Observer(例如即將進入休眠時執(zhí)行注冊回調(diào)_UIGestureRecognizerUpdateObserver用于手勢處理、回調(diào)為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer用于界面實時繪制更新)和多個Source1(例如context為CFMachPort的Source1用于接收硬件事件響應(yīng)進而分發(fā)到應(yīng)用程序一直到UIEvent)。

在主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)、Timer回調(diào)內(nèi)的。這些回調(diào)會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著,所以不會出現(xiàn)內(nèi)存泄漏,開發(fā)者也不必顯示創(chuàng)建 Pool 了。

自動釋放池的創(chuàng)建和釋放,銷毀的時機如下所示

  • kCFRunLoopEntry; // 進入runloop之前,創(chuàng)建一個自動釋放池
  • kCFRunLoopBeforeWaiting; // 休眠之前,銷毀自動釋放池,創(chuàng)建一個新的自動釋放池
  • kCFRunLoopExit; // 退出runloop之前,銷毀自動釋放池

事件響應(yīng)

蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統(tǒng)事件,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()。

當(dāng)一個硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收。這個過程的詳細情況可以參考這里。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進程。隨后蘋果注冊的那個 Source1 就會觸發(fā)回調(diào),并調(diào)用 _UIApplicationHandleEventQueue() 進行應(yīng)用內(nèi)部的分發(fā)。

_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進行處理或分發(fā),其中包括識別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等。通常事件比如 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調(diào)中完成的。

手勢識別

當(dāng)上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷。隨后系統(tǒng)將對應(yīng)的 UIGestureRecognizer 標記為待處理。

蘋果注冊了一個 Observer 監(jiān)測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver(),其內(nèi)部會獲取所有剛被標記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)。

當(dāng)有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時,這個回調(diào)都會進行相應(yīng)處理。

UI更新

如果打印App啟動之后的主線程RunLoop可以發(fā)現(xiàn)另外一個callout為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer,這個監(jiān)聽專門負責(zé)UI變化后的更新,比如修改了frame、調(diào)整了UI層級(UIView/CALayer)或者手動設(shè)置了setNeedsDisplay/setNeedsLayout之后就會將這些操作提交到全局容器。而這個Observer監(jiān)聽了主線程RunLoop的即將進入休眠和退出狀態(tài),一旦進入這兩種狀態(tài)則會遍歷所有的UI更新并提交進行實際繪制更新。

這個函數(shù)內(nèi)部的調(diào)用棧大概是這樣的:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

通常情況下這種方式是完美的,因為除了系統(tǒng)的更新,還可以利用setNeedsDisplay等方法手動觸發(fā)下一次RunLoop運行的更新。但是如果當(dāng)前正在執(zhí)行大量的邏輯運算可能UI的更新就會比較卡,因此facebook推出了AsyncDisplayKit來解決這個問題。AsyncDisplayKit其實是將UI排版和繪制運算盡可能放到后臺,將UI的最終更新操作放到主線程(這一步也必須在主線程完成),同時提供一套類UIView或CALayer的相關(guān)屬性,盡可能保證開發(fā)者的開發(fā)習(xí)慣。這個過程中AsyncDisplayKit在主線程RunLoop中增加了一個Observer監(jiān)聽即將進入休眠和退出RunLoop兩種狀態(tài),收到回調(diào)時遍歷隊列中的待處理任務(wù)一一執(zhí)行。

NSURLConnection

一旦啟動NSURLConnection以后就會不斷調(diào)用delegate方法接收數(shù)據(jù),這樣一個連續(xù)的的動作正是基于RunLoop來運行。
一旦NSURLConnection設(shè)置了delegate會立即創(chuàng)建一個線程com.apple.NSURLConnectionLoader,同時內(nèi)部啟動RunLoop并在NSDefaultMode模式下添加4個Source0。其中CFHTTPCookieStorage用于處理cookie ;CFMultiplexerSource負責(zé)各種delegate回調(diào)并在回調(diào)中喚醒delegate內(nèi)部的RunLoop(通常是主線程)來執(zhí)行實際操作。
早期版本的AFNetworking庫也是基于NSURLConnection實現(xiàn),為了能夠在后臺接收delegate回調(diào)AFNetworking內(nèi)部創(chuàng)建了一個空的線程并啟動了RunLoop,當(dāng)需要使用這個后臺線程執(zhí)行任務(wù)時AFNetworking通過**performSelector: onThread: **將這個任務(wù)放到后臺線程的RunLoop中。

當(dāng)調(diào)用 performSelector:onThread: 時,實際上其會創(chuàng)建一個 Timer 加到對應(yīng)的線程去,同樣的,如果對應(yīng)線程沒有 RunLoop 該方法也會失效。

GCD和RunLoop的關(guān)系

在RunLoop的源代碼中可以看到用到了GCD的相關(guān)內(nèi)容,但是RunLoop本身和GCD并沒有直接的關(guān)系。當(dāng)調(diào)用了dispatch_async(dispatch_get_main_queue(), <#^(void)block#>)時libDispatch會向主線程RunLoop發(fā)送消息喚醒RunLoop,RunLoop從消息中獲取block,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回調(diào)里執(zhí)行這個block。不過這個操作僅限于主線程,其他線程dispatch操作是全部由libDispatch驅(qū)動的。

更多RunLoop的實踐

滾動Scrollview導(dǎo)致定時器失效

在界面上有一個UIScrollview控件,如果此時還有一個定時器在執(zhí)行一個事件,你會發(fā)現(xiàn)當(dāng)你滾動Scrollview的時候,定時器會失效。

- (void)viewDidLoad {
    [super viewDidLoad];
    [self timer1];
    [self timer2];
}

//下面兩種添加定時器的方法效果相同,都是在主線程中添加定時器
- (void)timer1 {
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopDefaultModes];
}

- (void)timer2 {
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}

因為當(dāng)你滾動Scrollview的時候,RunLoop會切換到UITrackingRunLoopMode 模式,而定時器運行在defaultMode下面,系統(tǒng)一次只能處理一種模式的RunLoop,所以導(dǎo)致defaultMode下的定時器失效。

解決方法:

  • 把timer注冊到NSRunLoopCommonModes,它包含了defaultMode和trackingMode兩種模式。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
  • 使用GCD創(chuàng)建定時器,GCD創(chuàng)建的定時器不會受RunLoop的影響
    // 獲得隊列
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    // 創(chuàng)建一個定時器(dispatch_source_t本質(zhì)還是個OC對象)
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 設(shè)置定時器的各種屬性(幾時開始任務(wù),每隔多長時間執(zhí)行一次)
    // GCD的時間參數(shù),一般是納秒(1秒 == 10的9次方納秒)
    // 比當(dāng)前時間晚1秒開始執(zhí)行
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
    
    //每隔一秒執(zhí)行一次
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(self.timer, start, interval, 0);
    
    // 設(shè)置回調(diào)
    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"------------%@", [NSThread currentThread]);

    });
    
    // 啟動定時器
    dispatch_resume(self.timer);
圖片下載

由于圖片渲染到屏幕需要消耗較多資源,為了提高用戶體驗,當(dāng)用戶滾動Tableview的時候,只在后臺下載圖片,但是不顯示圖片,當(dāng)用戶停下來的時候才顯示圖片。

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

上面的代碼可以達到如下效果:
用戶點擊屏幕,在主線程中,三秒之后顯示圖片,但是當(dāng)用戶點擊屏幕之后,如果此時用戶又開始滾動textview,那么就算過了三秒,圖片也不會顯示出來,當(dāng)用戶停止了滾動,才會顯示圖片。
這是因為限定了方法setImage只能在NSDefaultRunLoopMode 模式下使用。而滾動textview的時候,程序運行在tracking模式下面,所以方法setImage不會執(zhí)行。

常駐線程

需要創(chuàng)建一個在后臺一直存在的程序,來做一些需要頻繁處理的任務(wù)。比如檢測網(wǎng)絡(luò)狀態(tài)等。

默認情況一個線程創(chuàng)建出來,運行完要做的事情,線程就會消亡。而程序啟動的時候,就創(chuàng)建的主線程已經(jīng)加入到RunLoop,所以主線程不會消亡。

這個時候我們就需要把自己創(chuàng)建的線程加到RunLoop中來,就可以實現(xiàn)線程常駐后臺。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)run
{
    NSLog(@"----------run----%@", [NSThread currentThread]);
    @autoreleasepool{
    /*如果不加這句,會發(fā)現(xiàn)runloop創(chuàng)建出來就掛了,因為runloop如果沒有CFRunLoopSourceRef事件源輸入或者定時器,就會立馬消亡。
      下面的方法給runloop添加一個NSport,就是添加一個事件源,也可以添加一個定時器,或者observer,讓runloop不會掛掉*/
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    
    // 方法1 ,2,3實現(xiàn)的效果相同,讓runloop無限期運行下去
    [[NSRunLoop currentRunLoop] run];
   }

    
    // 方法2
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    
    // 方法3
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
    
    NSLog(@"---------");
}

- (void)test
{
    NSLog(@"----------test----%@", [NSThread currentThread]);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}
- (void)run
{
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
    
    [[NSRunLoop currentRunLoop] run];
}

如果沒有實現(xiàn)添加NSPort或者NSTimer,會發(fā)現(xiàn)執(zhí)行完run方法,線程就會消亡,后續(xù)再執(zhí)行touchbegan方法無效。

我們必須保證線程不消亡,才可以在后臺接受時間處理

RunLoop 啟動前內(nèi)部必須要有至少一個 Timer/Observer/Source,所以在 [runLoop run] 之前先創(chuàng)建了一個新的 NSMachPort 添加進去了。通常情況下,調(diào)用者需要持有這個 NSMachPort (mach_port) 并在外部線程通過這個 port 發(fā)送消息到 RunLoop 內(nèi);但此處添加 port 只是為了讓 RunLoop 不至于退出,并沒有用于實際的發(fā)送消息。

可以發(fā)現(xiàn)執(zhí)行完了run方法,這個時候再點擊屏幕,可以不斷執(zhí)行test方法,因為線程self.thread一直常駐后臺,等待事件加入其中,然后執(zhí)行。

觀察事件狀態(tài),優(yōu)化性能

假設(shè)我們想實現(xiàn)cell的高度緩存計算,因為“計算cell的預(yù)緩存高度”的任務(wù)需要在最無感知的時刻進行,所以應(yīng)該同時滿足:

  • RunLoop 處于“空閑”狀態(tài) Mode
  • 當(dāng)這一次 RunLoop 迭代處理完成了所有事件,馬上要休眠時
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
    // TODO here
});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);
在其中的 TODO 位置,就可以開始任務(wù)的收集和分發(fā)了,當(dāng)然,不能忘記適時的移除這個 observer
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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