iOS RunLoop詳解

jackyshan_發表於2018-04-08

iOS RunLoop詳解

Runloop 是和執行緒緊密相關的一個基礎元件,是很多執行緒有關功能的幕後功臣。儘管在平常使用中幾乎不太會直接用到,理解 Runloop 有利於我們更加深入地理解 iOS 的多執行緒模型。

本文從如下幾個方面理解RunLoop的相關知識點。

  • RunLoop概念
  • RunLoop實現
  • RunLoop執行
  • RunLoop應用

RunLoop概念

RunLoop介紹

RunLoop 是什麼?RunLoop 還是比較顧名思義的一個東西,說白了就是一種迴圈,只不過它這種迴圈比較高階。一般的 while 迴圈會導致 CPU 進入忙等待狀態,而 RunLoop 則是一種“閒”等待,這部分可以類比 Linux 下的 epoll。當沒有事件時,RunLoop 會進入休眠狀態,有事件發生時, RunLoop 會去找對應的 Handler 處理事件。RunLoop 可以讓執行緒在需要做事的時候忙起來,不需要的話就讓執行緒休眠。

從程式碼上看,RunLoop其實就是一個物件,它的結構如下,原始碼看這裡

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* locked for accessing mode list */
    __CFPort _wakeUpPort;   // used for CFRunLoopWakeUp 核心向該埠傳送訊息可以喚醒runloop
    Boolean _unused;
    volatile _per_run_data *_perRunData; // reset for runs of the run loop
    pthread_t _pthread;             //RunLoop對應的執行緒
    uint32_t _winthread;
    CFMutableSetRef _commonModes;    //儲存的是字串,記錄所有標記為common的mode
    CFMutableSetRef _commonModeItems;//儲存所有commonMode的item(source、timer、observer)
    CFRunLoopModeRef _currentMode;   //當前執行的mode
    CFMutableSetRef _modes;          //儲存的是CFRunLoopModeRef
    struct _block_item *_blocks_head;//doblocks的時候用到
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};
複製程式碼

可見,一個RunLoop物件,主要包含了一個執行緒,若干個Mode,若干個commonMode,還有一個當前執行的Mode。

RunLoop與執行緒

當我們需要一個常駐執行緒,可以讓執行緒在需要做事的時候忙起來,不需要的話就讓執行緒休眠。我們就線上程裡面執行下面這個程式碼,一直等待訊息,執行緒就不會退出了。

do {
   //獲取訊息
   //處理訊息
} while (訊息 != 退出)
複製程式碼

上面的這種迴圈模型被稱作 Event Loop,事件迴圈模型在眾多系統裡都有實現,RunLoop 實際上就是一個物件,這個物件管理了其需要處理的事件和訊息,並提供了一個入口函式來執行上面 Event Loop 的邏輯。執行緒執行了這個函式後,就會一直處於這個函式內部 "接受訊息->等待->處理" 的迴圈中,直到這個迴圈結束(比如傳入 quit 的訊息),函式返回。

下圖描述了Runloop執行流程(基本描述了上面Runloop的核心流程,當然可以檢視官方The Run Loop Sequence of Events描述):

iOS RunLoop詳解

整個流程並不複雜(需要注意的就是_黃色_區域的訊息處理中並不包含source0,因為它在迴圈開始之初就會處理),整個流程其實就是一種Event Loop的實現,其他平臺均有類似的實現,只是這裡叫做RunLoop。

RunLoop與執行緒的關係如下圖

iOS RunLoop詳解

圖中展現了 Runloop 線上程中的作用:從 input source 和 timer source 接受事件,然後線上程中處理事件。

Runloop 和執行緒是繫結在一起的。每個執行緒(包括主執行緒)都有一個對應的 Runloop 物件。我們並不能自己建立 Runloop 物件,但是可以獲取到系統提供的 Runloop 物件。

主執行緒的 Runloop 會在應用啟動的時候完成啟動,其他執行緒的 Runloop 預設並不會啟動,需要我們手動啟動。

RunLoop Mode

Mode可以視為事件的管家,一個Mode管理著各種事件,它的結構如下:

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; //儲存所有需要監聽的port,比如_wakeUpPort,_timerPort都儲存在這個陣列中
    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()函式時必須指定Mode)。而通過CFRunloopRef對應結構體的定義可以很容易知道每種Runloop都可以包含若干個Mode,每個Mode又包含Source/Timer/Observer。每次呼叫Runloop的主函式__CFRunLoopRun()時必須指定一種Mode,這個Mode稱為 _currentMode**,當切換Mode時必須退出當前Mode,然後重新進入Runloop以保證不同Mode的Source/Timer/Observer互不影響。

iOS RunLoop詳解

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

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

  • NSDefaultRunLoopMode
  • NSConnectionReplyMode
  • NSModalPanelRunLoopMode
  • NSEventTrackingRunLoopMode
  • NSRunLoopCommonModes

iOS 中公開暴露出來的只有 NSDefaultRunLoopMode 和 NSRunLoopCommonModes。 NSRunLoopCommonModes 實際上是一個 Mode 的集合,預設包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode(注意:並不是說Runloop會執行在kCFRunLoopCommonModes這種模式下,而是相當於分別註冊了 NSDefaultRunLoopMode和 UITrackingRunLoopMode。當然你也可以通過呼叫CFRunLoopAddCommonMode()方法將自定義Mode放到 kCFRunLoopCommonModes組合)。

五種Mode的介紹如下圖:

iOS RunLoop詳解

RunLoop Source

Run Loop Source分為Source、Observer、Timer三種,他們統稱為ModeItem。

CFRunLoopSource

根據官方的描述,CFRunLoopSource是對input sources的抽象。CFRunLoopSource分source0和source1兩個版本,它的結構如下:

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits; //用於標記Signaled狀態,source0只有在被標記為Signaled狀態,才會被處理
    pthread_mutex_t _lock;
    CFIndex _order;         /* immutable */
    CFMutableBagRef _runLoops;
    union {
        CFRunLoopSourceContext version0;     /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};
複製程式碼

source0是App內部事件,由App自己管理的UIEvent、CFSocket都是source0。當一個source0事件準備執行的時候,必須要先把它標記為signal狀態,以下是source0的結構體:

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的。只包含了一個回撥(函式指標),它並不能主動觸發事件。使用時,你需要先呼叫 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然後手動呼叫 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。

source1由RunLoop和核心管理,source1帶有mach_port_t,可以接收核心訊息並觸發回撥,以下是source1的結構體

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除了包含回撥指標外包含一個mach port,Source1可以監聽系統埠和通過核心和其他執行緒通訊,接收、分發系統事件,它能夠主動喚醒RunLoop(由作業系統核心進行管理,例如CFMessagePort訊息)。官方也指出可以自定義Source,因此對於CFRunLoopSourceRef來說它更像一種協議,框架已經預設定義了兩種實現,如果有必要開發人員也可以自定義,詳細情況可以檢視官方文件

CFRunLoopObserver

CFRunLoopObserver是觀察者,可以觀察RunLoop的各種狀態,並丟擲回撥。

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可以觀察的狀態有如下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已經退出
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製程式碼

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

iOS RunLoop詳解

CFRunLoopTimer

CFRunLoopTimer是定時器,可以在設定的時間點丟擲回撥,它的結構如下:

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;  //標記fire狀態
    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 */
};
複製程式碼

另外根據官方文件的描述,CFRunLoopTimer和NSTimer是toll-free bridged的,可以相互轉換。

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 是定時器,可以在設定的時間點丟擲回撥
  • CFRunLoopTimer和NSTimer是toll-free bridged的,可以相互轉換

RunLoop實現

下面從以下3個方面介紹RunLoop的實現。

  • 獲取RunLoop
  • 新增Mode
  • 新增Run Loop Source

獲取RunLoop

從蘋果開放的API來看,不允許我們直接建立RunLoop物件,只能通過以下幾個函式來獲取RunLoop:

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

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

那麼RunLoop是什麼時候被建立的呢?

我們從下面幾個函式內部看看。

CFRunLoopGetCurrent
//取當前所線上程的RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    //傳入當前執行緒
    return _CFRunLoopGet0(pthread_self());
}
複製程式碼

在CFRunLoopGetCurrent函式內部呼叫了_CFRunLoopGet0(),傳入的引數是當前執行緒pthread_self()。這裡可以看出,CFRunLoopGetCurrent函式必須要線上程內部呼叫,才能獲取當前執行緒的RunLoop。也就是說子執行緒的RunLoop必須要在子執行緒內部獲取。

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函式內部也呼叫了_CFRunLoopGet0(),傳入的引數是主執行緒pthread_main_thread_np()。可以看出,CFRunLoopGetMain()不管在主執行緒還是子執行緒中呼叫,都可以獲取到主執行緒的RunLoop。

CFRunLoopGet0

前面兩個函式都是使用了CFRunLoopGet0實現傳入執行緒的函式,下面看下CFRunLoopGet0的結構是咋樣的。

static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFSpinLock_t loopsLock = CFSpinLockInit;
 
// t==0 is a synonym for "main thread" that always works
//根據執行緒取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);
        //建立一個臨時字典dict
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //建立主執行緒的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總是會被建立
    //從字典__CFRunLoops中獲取傳入執行緒t的runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);
    //如果沒有獲取到
    if (!loop) {
        //根據執行緒t建立一個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);
    }
    //如果傳入執行緒就是當前執行緒
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            //註冊一個回撥,當執行緒銷燬時,銷燬對應的RunLoop
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}
複製程式碼

這段程式碼可以得出以下結論:

  • RunLoop和執行緒的一一對應的,對應的方式是以key-value的方式儲存在一個全域性字典中
  • 主執行緒的RunLoop會在初始化全域性字典時建立
  • 子執行緒的RunLoop會在第一次獲取的時候建立,如果不獲取的話就一直不會被建立
  • RunLoop會線上程銷燬時銷燬

新增Mode

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

  • 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. 向當前RunLoop的common modes中新增一個mode。

CFRunLoopCopyCurrentMode Returns the name of the mode in which a given run loop is currently running. 返回當前執行的mode的name

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

我們沒有辦法直接建立一個CFRunLoopMode物件,但是我們可以呼叫CFRunLoopAddCommonMode傳入一個字串向RunLoop中新增Mode,傳入的字串即為Mode的名字,Mode物件應該是此時在RunLoop內部建立的。下面來看一下原始碼。

CFRunLoopAddCommonMode
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    __CFRunLoopLock(rl);
    //看rl中是否已經有這個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 */
            //這裡呼叫CFRunLoopAddSource/CFRunLoopAddObserver/CFRunLoopAddTimer的時候會呼叫
            //__CFRunLoopFindMode(rl, modeName, true),CFRunLoopMode物件在這個時候被建立
            CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
            CFRelease(set);
        }
    } else {
    }
    __CFRunLoopUnlock(rl);
}
複製程式碼

可以看得出:

  • modeName不能重複,modeName是mode的唯一識別符號
  • RunLoop的_commonModes陣列存放所有被標記為common的mode的名稱
  • 新增commonMode會把commonModeItems陣列中的所有source同步到新新增的mode中
  • CFRunLoopMode物件在CFRunLoopAddItemsToCommonMode函式中呼叫CFRunLoopFindMode時被建立
CFRunLoopCopyCurrentMode/CFRunLoopCopyAllModes

CFRunLoopCopyCurrentMode和CFRunLoopCopyAllModes的內部邏輯比較簡單,直接取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的程式碼結構如下:

//新增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一個新的複製給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到的陣列裡有資料
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rls};
            /* add new item to all common-modes */
            //則把set裡的所有mode都執行一遍__CFRunLoopAddItemToCommonModes函式
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
        //以上分支的邏輯就是,如果你往kCFRunLoopCommonModes裡面新增一個source,那麼所有_commonModes裡的mode都會新增這個source
    } else {
        //根據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對應起來
                    //可以理解為,source1可以通過核心向其埠傳送訊息來主動喚醒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的這段程式碼可以得出如下結論:

  • 如果modeName傳入kCFRunLoopCommonModes,則該source會被儲存到RunLoop的_commonModeItems中
  • 如果modeName傳入kCFRunLoopCommonModes,則該source會被新增到所有commonMode中
  • 如果modeName傳入的不是kCFRunLoopCommonModes,則會先查詢該Mode,如果沒有,會建立一個
  • 同一個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 {
        //根據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);
            //根據source版本做對應的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的內部邏輯和新增source大體類似。

區別在於observer和timer只能被新增到一個RunLoop的一個或者多個mode中,比如一個timer被新增到主執行緒的RunLoop中,則不能再把該timer新增到子執行緒的RunLoop,而source沒有這個限制,不管是哪個RunLoop,只要mode中沒有,就可以新增。

這個區別在文章最開始的結構體中也可以發現,CFRunLoopSource結構體中有儲存RunLoop物件的陣列,而CFRunLoopObserver和CFRunLoopTimer只有單個RunLoop物件。

RunLoop執行

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

  • void CFRunLoopRun(void)

在預設的mode下執行當前執行緒的RunLoop。

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

在指定mode下執行當前執行緒的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函式中呼叫了CFRunLoopRunSpecific函式,runloop引數傳入當前RunLoop物件,modeName引數傳入kCFRunLoopDefaultMode。驗證了前面文件的解釋。

CFRunLoopRunInMode

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

在CFRunLoopRunInMode函式中也呼叫了CFRunLoopRunSpecific函式,runloop引數傳入當前RunLoop物件,modeName引數繼續傳遞CFRunLoopRunInMode傳入的modeName。也驗證了前面文件的解釋。

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

這2個函式都看不出來RunLoop是怎麼run起來的。

接下來我們繼續探索一下CFRunLoopRunSpecific函式裡面都幹了什麼,看看RunLoop具體是怎麼run的。

CFRunLoopRunSpecific

/*
 * 指定mode執行runloop
 * @param rl 當前執行的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);
    //根據modeName找到本次執行的mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    //如果沒找到 || mode中沒有註冊任何事件,則就此停止,不進入迴圈
    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的內部邏輯,我們可以得出:

  • 如果指定了一個不存在的mode來執行RunLoop,那麼會失敗,mode不會被建立,所以這裡傳入的mode必須是存在的
  • 如果指定了一個mode,但是這個mode中不包含任何modeItem,那麼RunLoop也不會執行,所以必須要* 傳入至少包含一個modeItem的mode
  • 在進入run loop之前通知observer,狀態為kCFRunLoopEntry
  • 在退出run loop之後通知observer,狀態為kCFRunLoopExit

RunLoop的執行的最核心函式是__CFRunLoopRun,接下來我們分析__CFRunLoopRun的原始碼。

__CFRunLoopRun

這段程式碼比較長,請做好心理準備,我已經加了比較詳細的註釋。本節開頭的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種狀態
 */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    //獲取系統啟動後的CPU執行時間,用於控制超時時間
    uint64_t startTSR = mach_absolute_time();
    
    //如果RunLoop或者mode是stop狀態,則直接return,不進入迴圈
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
        return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;
        return kCFRunLoopRunStopped;
    }
    
    //mach埠,在核心中,訊息在埠之間傳遞。 初始為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埠賦值為主執行緒收發訊息的埠
    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管理的定時器,用於實現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為超時時間,超時時執行__CFRunLoopTimeout函式
    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狀態,用於return
    int32_t retVal = 0;
    do {
        //初始化一個存放核心訊息的緩衝池
        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
        //取所有需要監聽的port
        __CFPortSet waitSet = rlm->_portSet;
        
        //設定RunLoop為可以被喚醒狀態
        __CFRunLoopUnsetIgnoreWakeUps(rl);
        
        //2.通知observer,即將觸發timer回撥,處理timer事件
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        //3.通知observer,即將觸發Source0回撥
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
        //執行加入當前runloop的block
        __CFRunLoopDoBlocks(rl, rlm);
        
        //4.處理source0事件
        //有事件處理返回true,沒有事件返回false
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            //執行加入當前runloop的block
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        //如果沒有Sources0事件處理 並且 沒有超時,poll為false
        //如果有Sources0事件處理 或者 超時,poll都為true
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
        //第一次do..whil迴圈不會走該分支,因為didDispatchPortLastTime初始化是true
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            //從緩衝區讀取訊息
            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);
        //設定RunLoop為休眠狀態
        __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
        //這裡有個內迴圈,用於接收等待埠的訊息
        //進入此迴圈後,執行緒進入休眠,直到收到新訊息才跳出該迴圈,繼續執行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的休眠狀態
        __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重新迴圈
            // 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執行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時傳入的引數,處理完事件就返回
            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;
        }
        //除了上面這幾種情況,都繼續迴圈
    } while (0 == retVal);
    
    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }
    
    return retVal;
}
複製程式碼

__CFRunLoopServiceMachPort

第7步呼叫了__CFRunLoopServiceMachPort函式,這個函式在run loop中起到了至關重要的作用,下面給出了詳細註釋。

/**
 *  接收指定核心埠的訊息
 *
 *  @param port        接收訊息的埠
 *  @param buffer      訊息緩衝區
 *  @param buffer_size 訊息緩衝區大小
 *  @param livePort    暫且理解為活動的埠,接收訊息成功時候值為msg->msgh_local_port,超時時為MACH_PORT_NULL
 *  @param timeout     超時時間,單位是ms,如果超時,則RunLoop進入休眠狀態
 *
 *  @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;  //源(發出的訊息)或者目標(接收的訊息)
        msg->msgh_remote_port = MACH_PORT_NULL; //目標(發出的訊息)或者源(接收的訊息)
        msg->msgh_size = buffer_size;  //訊息緩衝區大小,單位是位元組
        msg->msgh_id = 0;  //唯一id
       
        if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
        
        //通過mach_msg傳送或者接收的訊息都是指標,
        //如果直接傳送或者接收訊息體,會頻繁進行記憶體複製,損耗效能
        //所以XNU使用了單一核心的方式來解決該問題,所有核心元件都共享同一個地址空間,因此傳遞訊息時候只需要傳遞訊息的指標
        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);
        
        //接收/傳送訊息成功,給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
        //此時釋放緩衝區,把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
        //如果接收緩衝區太小,則將過大的訊息放在佇列中,並且出錯返回MACH_RCV_TOO_LARGE,
        //這種情況下,只返回訊息頭,呼叫者可以分配更多的記憶體
        if (MACH_RCV_TOO_LARGE != ret) break;
        //此處給buffer分配更大記憶體
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
        if (originalBuffer) *buffer = NULL;
        originalBuffer = false;
        *buffer = realloc(*buffer, buffer_size);
    }
    HALT;
    return false;
}
複製程式碼

小結

RunLoop實際很簡單,它是一個物件,它和執行緒是一一對應的,每個執行緒都有一個對應的RunLoop物件,主執行緒的RunLoop會在程式啟動時自動建立,子執行緒需要手動獲取來建立。

RunLoop執行的核心是一個do..while..迴圈,遍歷所有需要處理的事件,如果有事件處理就讓執行緒工作,沒有事件處理則讓執行緒休眠,同時等待事件到來。

RunLoop應用

iOS RunLoop詳解

在開發過程中幾乎所有的操作都是通過Call out進行回撥的(無論是Observer的狀態通知還是Timer、Source的處理),而系統在回撥時通常使用如下幾個函式進行回撥(換句話說你的程式碼其實最終都是通過下面幾個函式來負責呼叫的,即使你自己監聽Observer也會先呼叫下面的函式然後間接通知你,所以在呼叫堆疊中經常看到這些函式):

    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會建立AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
 
        /// 2. 通知 Observers: 即將觸發 Timer 回撥。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即將觸發 Source (非基於port的,Source0) 回撥。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 4. 觸發 Source0 (非基於port的) 回撥。
        __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喚醒的,回撥Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
 
        /// 9. 如果是被dispatch喚醒的,執行所有呼叫 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函式CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION呼叫):

iOS RunLoop詳解

NSTimer 與 GCD Timer、CADisplayLink

NSTimer

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

NSTimer 其實就是 CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個 NSTimer 註冊到 RunLoop 後,RunLoop 會為其重複的時間點註冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop為了節省資源,並不會在非常準確的時間點回撥這個Timer。Timer 有個屬性叫做 Tolerance (寬容度),標示了當時間點到後,容許有多少最大誤差。由於 NSTimer 的這種機制,因此 NSTimer 的執行必須依賴於 RunLoop,如果沒有 RunLoop,NSTimer 是不會執行的。

如果某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回撥也會跳過去,不會延後執行。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。

GCD Timer

GCD 則不同,GCD 的執行緒管理是通過系統來直接管理的。GCD Timer 是通過 dispatch port 給 RunLoop 傳送訊息,來使 RunLoop 執行相應的 block,如果所線上程沒有 RunLoop,那麼 GCD 會臨時建立一個執行緒去執行 block,執行完之後再銷燬掉,因此 GCD 的 Timer 是不依賴 RunLoop 的。

至於這兩個 Timer 的準確性問題,如果不在 RunLoop 的執行緒裡面執行,那麼只能使用 GCD Timer,由於 GCD Timer 是基於 MKTimer(mach kernel timer),已經很底層了,因此是很準確的。

如果在 RunLoop 的執行緒裡面執行,由於 GCD Timer 和 NSTimer 都是通過 port 傳送訊息的機制來觸發 RunLoop 的,因此準確性差別應該不是很大。如果執行緒 RunLoop 阻塞了,不管是 GCD Timer 還是 NSTimer 都會存在延遲問題。

CADisplayLink

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

AutoreleasePool

AutoreleasePool是另一個與RunLoop相關討論較多的話題。其實從RunLoop原始碼分析,AutoreleasePool與RunLoop並沒有直接的關係,之所以將兩個話題放到一起討論最主要的原因是因為在iOS應用啟動後會註冊兩個Observer管理和維護AutoreleasePool。不妨在應用程式剛剛啟動時列印currentRunLoop可以看到系統預設註冊了很多個Observer,其中有兩個Observer的callout都是** _ wrapRunLoopWithAutoreleasePoolHandler**,這兩個是和自動釋放池相關的兩個監聽。

<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會監聽RunLoop的進入,它會回撥objc_autoreleasePoolPush()向當前的AutoreleasePoolPage增加一個哨兵物件標誌建立自動釋放池。這個Observer的order是-2147483647優先順序最高,確保發生在所有回撥操作之前。 第二個Observer會監聽RunLoop的進入休眠和即將退出RunLoop兩種狀態,在即將進入休眠時會呼叫objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根據情況從最新加入的物件一直往前清理直到遇到哨兵物件。而在即將退出RunLoop時會呼叫objc_autoreleasePoolPop() 釋放自動自動釋放池內物件。這個Observer的order是2147483647,優先順序最低,確保發生在所有回撥操作之後。 主執行緒的其他操作通常均在這個AutoreleasePool之內(main函式中),以儘可能減少記憶體維護操作(當然你如果需要顯式釋放【例如迴圈】時可以自己建立AutoreleasePool否則一般不需要自己建立)。 其實在應用程式啟動後系統還註冊了其他Observer(例如即將進入休眠時執行註冊回撥_UIGestureRecognizerUpdateObserver用於手勢處理、回撥為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer用於介面實時繪製更新)和多個Source1(例如context為CFMachPort的Source1用於接收硬體事件響應進而分發到應用程式一直到UIEvent)。

在主執行緒執行的程式碼,通常是寫在諸如事件回撥、Timer回撥內的。這些回撥會被 RunLoop 建立好的 AutoreleasePool 環繞著,所以不會出現記憶體洩漏,開發者也不必顯示建立 Pool 了。

自動釋放池的建立和釋放,銷燬的時機如下所示

  • kCFRunLoopEntry; // 進入runloop之前,建立一個自動釋放池
  • kCFRunLoopBeforeWaiting; // 休眠之前,銷燬自動釋放池,建立一個新的自動釋放池
  • kCFRunLoopExit; // 退出runloop之前,銷燬自動釋放池

事件響應

蘋果註冊了一個 Source1 (基於 mach port 的) 用來接收系統事件,其回撥函式為 __IOHIDEventSystemClientQueueCallback()。

當一個硬體事件(觸控/鎖屏/搖晃等)發生後,首先由 IOKit.framework 生成一個 IOHIDEvent 事件並由 SpringBoard 接收。這個過程的詳細情況可以參考這裡。SpringBoard 只接收按鍵(鎖屏/靜音等),觸控,加速,接近感測器等幾種 Event,隨後用 mach port 轉發給需要的App程式。隨後蘋果註冊的那個 Source1 就會觸發回撥,並呼叫 _UIApplicationHandleEventQueue() 進行應用內部的分發。

_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理幷包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理螢幕旋轉/傳送給 UIWindow 等。通常事件比如 UIButton 點選、touchesBegin/Move/End/Cancel 事件都是在這個回撥中完成的。

手勢識別

當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會呼叫 Cancel 將當前的 touchesBegin/Move/End 系列回撥打斷。隨後系統將對應的 UIGestureRecognizer 標記為待處理。

蘋果註冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回撥函式是 _UIGestureRecognizerUpdateObserver(),其內部會獲取所有剛被標記為待處理的 GestureRecognizer,並執行GestureRecognizer的回撥。

當有 UIGestureRecognizer 的變化(建立/銷燬/狀態改變)時,這個回撥都會進行相應處理。

UI更新

如果列印App啟動之後的主執行緒RunLoop可以發現另外一個callout為**_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv**的Observer,這個監聽專門負責UI變化後的更新,比如修改了frame、調整了UI層級(UIView/CALayer)或者手動設定了setNeedsDisplay/setNeedsLayout之後就會將這些操作提交到全域性容器。而這個Observer監聽了主執行緒RunLoop的即將進入休眠和退出狀態,一旦進入這兩種狀態則會遍歷所有的UI更新並提交進行實際繪製更新。

這個函式內部的呼叫棧大概是這樣的:

_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];
複製程式碼

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

NSURLConnection

一旦啟動NSURLConnection以後就會不斷呼叫delegate方法接收資料,這樣一個連續的的動作正是基於RunLoop來執行。 一旦NSURLConnection設定了delegate會立即建立一個執行緒com.apple.NSURLConnectionLoader,同時內部啟動RunLoop並在NSDefaultMode模式下新增4個Source0。其中CFHTTPCookieStorage用於處理cookie ;CFMultiplexerSource負責各種delegate回撥並在回撥中喚醒delegate內部的RunLoop(通常是主執行緒)來執行實際操作。 早期版本的AFNetworking庫也是基於NSURLConnection實現,為了能夠在後臺接收delegate回撥AFNetworking內部建立了一個空的執行緒並啟動了RunLoop,當需要使用這個後臺執行緒執行任務時AFNetworking通過**performSelector: onThread: **將這個任務放到後臺執行緒的RunLoop中。

當呼叫 performSelector:onThread: 時,實際上其會建立一個 Timer 加到對應的執行緒去,同樣的,如果對應執行緒沒有 RunLoop 該方法也會失效。

GCD和RunLoop的關係

在RunLoop的原始碼中可以看到用到了GCD的相關內容,但是RunLoop本身和GCD並沒有直接的關係。當呼叫了dispatch_async(dispatch_get_main_queue(), <#^(void)block#>)時libDispatch會向主執行緒RunLoop傳送訊息喚醒RunLoop,RunLoop從訊息中獲取block,並且在__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__回撥裡執行這個block。不過這個操作僅限於主執行緒,其他執行緒dispatch操作是全部由libDispatch驅動的。

更多RunLoop的實踐

滾動Scrollview導致定時器失效

在介面上有一個UIScrollview控制元件,如果此時還有一個定時器在執行一個事件,你會發現當你滾動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];
}
複製程式碼

因為當你滾動Scrollview的時候,RunLoop會切換到UITrackingRunLoopMode 模式,而定時器執行在defaultMode下面,系統一次只能處理一種模式的RunLoop,所以導致defaultMode下的定時器失效。

解決方法:

  • 把timer註冊到NSRunLoopCommonModes,它包含了defaultMode和trackingMode兩種模式。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
複製程式碼
  • 使用GCD建立定時器,GCD建立的定時器不會受RunLoop的影響
    // 獲得佇列
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    // 建立一個定時器(dispatch_source_t本質還是個OC物件)
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 設定定時器的各種屬性(幾時開始任務,每隔多長時間執行一次)
    // GCD的時間引數,一般是納秒(1秒 == 10的9次方納秒)
    // 比當前時間晚1秒開始執行
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
    
    //每隔一秒執行一次
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(self.timer, start, interval, 0);
    
    // 設定回撥
    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"------------%@", [NSThread currentThread]);

    });
    
    // 啟動定時器
    dispatch_resume(self.timer);
複製程式碼
圖片下載

由於圖片渲染到螢幕需要消耗較多資源,為了提高使用者體驗,當使用者滾動Tableview的時候,只在後臺下載圖片,但是不顯示圖片,當使用者停下來的時候才顯示圖片。

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

上面的程式碼可以達到如下效果: 使用者點選螢幕,在主執行緒中,三秒之後顯示圖片,但是當使用者點選螢幕之後,如果此時使用者又開始滾動textview,那麼就算過了三秒,圖片也不會顯示出來,當使用者停止了滾動,才會顯示圖片。 這是因為限定了方法setImage只能在NSDefaultRunLoopMode 模式下使用。而滾動textview的時候,程式執行在tracking模式下面,所以方法setImage不會執行。

常駐執行緒

需要建立一個在後臺一直存在的程式,來做一些需要頻繁處理的任務。比如檢測網路狀態等。

預設情況一個執行緒建立出來,執行完要做的事情,執行緒就會消亡。而程式啟動的時候,就建立的主執行緒已經加入到RunLoop,所以主執行緒不會消亡。

這個時候我們就需要把自己建立的執行緒加到RunLoop中來,就可以實現執行緒常駐後臺。

- (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{
    /*如果不加這句,會發現runloop建立出來就掛了,因為runloop如果沒有CFRunLoopSourceRef事件源輸入或者定時器,就會立馬消亡。
      下面的方法給runloop新增一個NSport,就是新增一個事件源,也可以新增一個定時器,或者observer,讓runloop不會掛掉*/
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    
    // 方法1 ,2,3實現的效果相同,讓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];
}
複製程式碼

如果沒有實現新增NSPort或者NSTimer,會發現執行完run方法,執行緒就會消亡,後續再執行touchbegan方法無效。

我們必須保證執行緒不消亡,才可以在後臺接受時間處理

RunLoop 啟動前內部必須要有至少一個 Timer/Observer/Source,所以在 [runLoop run] 之前先建立了一個新的 NSMachPort 新增進去了。通常情況下,呼叫者需要持有這個 NSMachPort (mach_port) 並在外部執行緒通過這個 port 傳送訊息到 RunLoop 內;但此處新增 port 只是為了讓 RunLoop 不至於退出,並沒有用於實際的傳送訊息。

可以發現執行完了run方法,這個時候再點選螢幕,可以不斷執行test方法,因為執行緒self.thread一直常駐後臺,等待事件加入其中,然後執行。

觀察事件狀態,優化效能

假設我們想實現cell的高度快取計算,因為“計算cell的預快取高度”的任務需要在最無感知的時刻進行,所以應該同時滿足:

  • RunLoop 處於“空閒”狀態 Mode
  • 當這一次 RunLoop 迭代處理完成了所有事件,馬上要休眠時
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
    // TODO here
});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);
在其中的 TODO 位置,就可以開始任務的收集和分發了,當然,不能忘記適時的移除這個 observer
複製程式碼

關注我

歡迎關注公眾號:jackyshan,技術乾貨首發微信,第一時間推送。

iOS RunLoop詳解

相關文章