RunLoop 原始碼閱讀

ZenonHuang發表於2018-04-17

前言

這一篇文章主要在於 Run Loop 原始碼的閱讀,內容有點長,需要一些基礎。

Event Loop

Run Loop 是一個 iOS 開發裡的基礎概念,它並非獨有的機制,很多系統和框架都有類似的實現,Run Loop 是 Event Loop (事件迴圈)機制的在 iOS 平臺的一種實現。 查閱 wikipedia 有關 Event Loop 的描述:

在電腦科學裡, Event Loop / Run Loop 是一個用於等待和傳送訊息/事件的程式結構,在程式中等待和派發一系列事件或者訊息。它通過向“事件提供者”發出請求來工作,通常會阻塞請求,直到事件到達,然後呼叫相應的事件處理程式。

Event Driven

說到 Event Loop ,其實還應該瞭解到 Event-Driven (事件驅動)。

Event-Driven 的出現,在於解決圖形介面和使用者互動的問題:

通常 GUI 程式的互動事件執行是由使用者來控制的,無法預測它發生的節點,對應這樣的情況,需要採用 Event-Driven 的程式設計方法。

Event-Driven 的實現原理,基本就是使用 Event Loop 完成。Event-Driven 程式的執行,可以概括成:

啟動 ——> 事件迴圈(即等待事件發生並處理之)。

在 GUI 的設計場景下,一般寫程式碼會是下面的思維:

使用者輸入 -> 事件響應 -> 程式碼執行 -> 重新整理頁面狀態

Event

我們一直在說 Event Loop 和 Event-Driven 。那什麼是 Event (事件) 呢?

在 Event-Driven 中,可以把一切行為都抽象為 Event 。例如: IO 操作完成,使用者點選按鈕,一個圖片載入完成,文字框的文字改變等等情況,都可以看作是一個 Event 。

Event Handler

當 Event 被放到 Event Loop 裡進行處理的時候,會呼叫預先註冊過的程式碼對 Event 做處理。這就叫 Event Handler 。

Event Handler 其實就是對 Event 的響應,可以叫做事件回撥,事件處理,事件響應,都是一樣的概念。

這裡需要注意的是,一個 Event 並不一定有 Event Handler .

Event Loop 解決了什麼問題

一般來說,操作分為同步和非同步。

同步操作,是一個接一個的處理。等前一個處理完,再執行下一個。那麼在一些耗時任務上,比如有很多 I/O 操作 或者 網路請求 的任務,執行緒就會有長時間在等待任務結果。

非同步操作,是不用等待執行結果的,可以直接在這期間執行另外的任務。等到任務結果出來之後,再進行處理。

實際上 Event Loop 就是實現非同步的一種方法。

對於有回撥的 Event,執行緒不用一直等待任務的結果出來再去執行下一個。而是等 Event 被加入到 Event Loop 時,再去執行。如果一個 Event 也沒有,那執行緒就會休眠,避免浪費資源。

如果沒有 Event Loop 來實現非同步操作,那我們的程式會很容易出現卡頓。

擴充套件 : JavaScript 在單執行緒條件下執行,可以完成非同步操作,也是基於 Event Loop 機制。 建議可以參考 JavaScript非同步程式設計 的內容來理解,更以幫助我們觸類旁通,學習到通用的知識。

Run Loop 實現

網上目前有關 Run Loop 的文章, 10 篇裡面可能有 8 篇都是重複了 深入理解RunLoop 中的程式碼。

然而這都是經過作者大量簡化過的版本,隱藏了大量的細節。

其實從細節裡面,我們一樣可以學習到很多東西,不妨嘗試去閱讀一下。

我們知道 CFRunLoopRef 的程式碼是開源的,可以檢視原始碼來看它的實現,我選擇的版本是 CF-1153.18 中的 CFRunLoop.c 。

獲取 Run Loop

由於蘋果不允許我們直接建立 RunLoop,只提供 2 個獲取操作的函式:

  • CFRunLoopGetMain :
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;
}

複製程式碼
  • CFRunLoopGetCurrent :
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}


複製程式碼

CHECK_FOR_FORK()

在兩個函式裡,都有使用了 CHECK_FOR_FORK() 。

它應該是屬於多程式情況下的一個斷言。

Threading Programming Guide 中,有這麼一段話:

Warning: When launching separate processes using the fork function, you must always follow a call to fork with a call to exec or a similar function. Applications that depend on the Core Foundation, Cocoa, or Core Data frameworks (either explicitly or implicitly) must make a subsequent call to an exec function or those frameworks may behave improperly.

也就是說,當通過 fork 啟動一個新程式的時候,你必須要接著呼叫一個 exec 或類似的函式。而依賴於 Core Founadtion / Cocoa / Core Data 框架的應用,必須呼叫 exec 函式,否則這些框架也許不能正確的工作。

所以為了保證安全,使用 CHECK_FOR_FORK 進行檢查。

FORK

這裡簡單提一下 fork 。

在 UNIX 中,用 fork 來建立子程式,呼叫 fork( ) 的程式被稱為父程式,新程式是子程式,並且幾乎是父程式的完全複製(變數、檔案控制程式碼、共享記憶體訊息等相同,但 process id 不同)。

因為子程式和父程式基本是一樣的,要想讓子程式去執行其他不同的程式,子程式就需要呼叫 exec ,把自身替換為新的程式,其中process id不變,但原來程式的程式碼段、堆疊段、資料段被新的內容取代,來執行新的程式。

這樣 fork 和 exec 就成為一種組合。

而在 iOS 這樣的類 UNIX 系統裡,基本上也都要通過 fork 的形式來建立新的程式。

假如沒有執行完 exec ,那麼執行的程式碼段等內容,還是父程式裡的,出現問題可以說百分之百。這就是 CHECK_FOR_FORK 檢查的目的。

Thread-specific data

為了幫助理解,還需要先說 Thread-specific data (TSD),它可以叫作 執行緒私有資料 , 這個概念來自於 unix 之中。

它是儲存和查詢與某個執行緒相關資料的一種機制:

程式內的所有執行緒,共享程式的資料空間,因此全域性變數為所有執行緒所共有。 而有時執行緒也需要儲存自己的私有資料,這時可以建立執行緒私有資料(Thread-specific Data)TSD來解決。 線上程內部,私有資料可以被各個函式訪問,但對其他執行緒是遮蔽的。例如我們常見的變數errno,它返回標準的出錯資訊。它顯然不能是一個區域性變數,幾乎每個函式都應該可以呼叫它;但它又不能是一個全域性變數。

Pthreads 裡,把它叫做 Thread-local storage (執行緒私有儲存) , 有以下幾個相關的操作函式:

- pthread_key_create(): 分配用於標識程式中執行緒特定資料的pthread_key_t型別的鍵
- pthread_key_delete(): 銷燬現有執行緒特定資料鍵
- pthread_setspecific(): 為指定執行緒的特定資料鍵設定繫結的值
- pthread_getspecific(): 獲取呼叫執行緒的鍵繫結值,並將該繫結儲存在 value 指向的位置中
複製程式碼

在蘋果的平臺上,基本就是利用上面操作實現 TSD 。

RunLoop 實際上就屬於 TSD 的裡儲存的一種資料。所以我們講, RunLoop 和執行緒是一一對應的。而 RunLoop 會線上程銷燬時,跟著一起清理,也是由於執行緒私有資料的機制。

TSD 對應儲存的 key 有相關的解構函式,執行緒退出時,解構函式函式就會按照作業系統,實現定義的順序被呼叫。所以在 CFSetTSD 會有一個解構函式的引數位置。

CFGetTSD / CFSetTSD

關於 CFGetTSD / CFSetTSD , 在 ForFoundationOnly.h 找到定義:

// ---- Thread-specific data --------------------------------------------

// Get some thread specific data from a pre-assigned slot.
CF_EXPORT void *_CFGetTSD(uint32_t slot);

// Set some thread specific data in a pre-assigned slot. Don't pick a random value. Make sure you're using a slot that is unique. Pass in a destructor to free this data, or NULL if none is needed. Unlike pthread TSD, the destructor is per-thread.
CF_EXPORT void *_CFSetTSD(uint32_t slot, void *newVal, void (*destructor)(void *));
複製程式碼

TSD 也就是 thread specific data 的縮寫了。

按照註釋,說明 CFGetTSD 的作用是 -- 從預先賦值的位置,得到 TSD

上面也說明了 CFSetTSD 的作用 -- 在預先位置設定 TSD 。 這個資料不可以是隨機的值,並保證你使用的位置有唯一性。如果需要釋放這個資料,就傳入解構函式;如果不需要釋放,則傳入NULL。和 pthread TSD 不同的是,這一個解構函式是每一個執行緒都有的。

在上面的CFRunLoopGetCurrent裡,是這麼使用 _CFGetTSD 的:

 CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
複製程式碼

這裡的 slot 值,一般對應的,都應該是類似 __CFTSDKeyRunLoop 的列舉型別的關鍵字。

CFTSDTable

CFPlatform.c 找到 CFGetTSD / CFSetTSD 具體的實現,發現兩者其實都依靠了 CFTSDTable 型別的一個 table 實現。

CFTSDTable 是一個儲存 TSD 資料的結構體:

// Data structure to hold TSD data, cleanup functions for each
typedef struct __CFTSDTable {
    uint32_t destructorCount;
    uintptr_t data[CF_TSD_MAX_SLOTS];
    tsdDestructor destructors[CF_TSD_MAX_SLOTS];
} __CFTSDTable;

複製程式碼

它擁有兩個陣列: data 儲存私有資料, destructors 儲存釋放函式 . 還有一個 destructorCount ,它顧名思義就是 destructors 陣列的數量。

CFGetTSD 主要是取了 table ,獲取 table 的 data 陣列,按 slot 索引取值。

CFSetTSD 的作用,就是根據 CFTSDTable 的結構,分別是往 table 裡設定 data 陣列 slot 位置的值,以及 destructors 陣列 slot 位置的值:

    void *oldVal = (void *)table->data[slot];
    
    table->data[slot] = (uintptr_t)newVal;
    table->destructors[slot] = destructor;

複製程式碼

CFRunLoopGet0

無論是 CFRunLoopGetMain 還是 CFRunLoopGetCurrent ,兩者呼叫了 CFRunLoopGet0 :

static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFLock_t loopsLock = CFLockInit;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {// kNilPthreadT 是一個靜態變數為 0
	   t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
	CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
	CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
	CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
	if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
	    CFRelease(dict);
	}
		CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
	CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
	loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
	if (!loop) {
	    CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
	    loop = newLoop;
	}
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
	CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

複製程式碼

這裡對主要的流程做一下解釋:

  1. 第一次進入,無論 t 為主執行緒或者子執行緒。因為 __CFRunLoops 為 null ,所以會建立一個 mainLoop.
  2. 根據傳遞進來的 t ,建立對應的 loop 。t 作為 key,loop 作為 value ,儲存到 __CFRunLoops 裡。如果已經有了對應 loop 存在,則不建立。
  3. 判斷 t 是否為當前執行緒。如果是當前執行緒,就會利用 CFSetTSD 在 CFTSDKeyRunLoop/CFTSDKeyRunLoopCntr 的位置做設定。

注意:

在瞭解完 CFSetTSD 的作用, CFTSDKeyRunLoop 設定的意思就很清楚: 在 CFTSDKeyRunLoop 位置,儲存 loop , 但不對 loop 設定解構函式。

直接對於 loop 的設定,其實這裡已經完成了。

網路文章大部分,直接就說在 CFTSDKeyRunLoopCntr 設定了清理 loop 的回撥。

對於為什麼可以釋放 loop ,卻避而不談。

大家想過沒有 :

在 CFTSDKeyRunLoopCntr 位置,給出的引數是 PTHREAD_DESTRUCTOR_ITERATIONS - 1 PTHREAD_DESTRUCTOR_ITERATIONS 表示的,是執行緒退出時,作業系統實現試圖銷燬執行緒私有資料的最大次數。

試圖銷燬次數,和 CFFinalizeRunLoop 這個解構函式,是怎麼關聯起來的?又是怎麼被呼叫的?

在前面,說過了 CFTSDTable ,實際上在 CFTSDGetTable() 裡面,就做了相關 TSD 的設定:

// Get or initialize a thread local storage. It is created on demand.
static __CFTSDTable *__CFTSDGetTable() {
   ...
        pthread_key_init_np(CF_TSD_KEY, __CFTSDFinalize);
   ...
}

複製程式碼

通過 CF_TSD_KEY ,指定了對應的解構函式 CFTSDFinalize 。

而 CFTSDFinalize 的程式碼如下:

static void __CFTSDFinalize(void *arg) {
   // Set our TSD so we're called again by pthreads. It will call the destructor PTHREAD_DESTRUCTOR_ITERATIONS times as long as a value is set in the thread specific data. We handle each case below.
    __CFTSDSetSpecific(arg);

    if (!arg || arg == CF_TSD_BAD_PTR) {
        // We've already been destroyed. The call above set the bad pointer again. Now we just return.
        return;
    }
    // On first calls invoke destructor. Later we destroy the data.
    // Note that invocation of the destructor may cause a value to be set again in the per-thread data slots. The destructor count and destructors are preserved.  
    // This logic is basically the same as what pthreads does. We just skip the 'created' flag.
    for (int32_t i = 0; i < CF_TSD_MAX_SLOTS; i++) {
        if (table->data[i] && table->destructors[i]) {
            uintptr_t old = table->data[i];
            table->data[i] = (uintptr_t)NULL;
            table->destructors[i]((void *)(old));
        }
    }
    
    if (table->destructorCount == PTHREAD_DESTRUCTOR_ITERATIONS - 1) {    // On PTHREAD_DESTRUCTOR_ITERATIONS-1 call, destroy our data
        free(table);
        
        // Now if the destructor is called again we will take the shortcut at the beginning of this function.
        __CFTSDSetSpecific(CF_TSD_BAD_PTR);
        return;
    }
}
複製程式碼

我們可以看到,table 會迴圈遍歷 data 和 destructors 的資料,並且把 old 變數作為 destructors 裡函式的引數。

這就是執行緒退出時,會呼叫到 Run Loop 銷燬函式的原因。

同時也由於 table 是從 0 開始遍歷,所以會根據列舉值的大小,來決定銷燬呼叫的順序的。

我們可以在 CFInternal.h 中找到相關列舉定義:

// Foundation uses 20-40
// Foundation knows about the value of __CFTSDKeyAutoreleaseData1
enum {
		__CFTSDKeyAllocator = 1,
		__CFTSDKeyIsInCFLog = 2,
		__CFTSDKeyIsInNSCache = 3,
		__CFTSDKeyIsInGCDMainQ = 4,
		__CFTSDKeyICUConverter = 7,
		__CFTSDKeyCollatorLocale = 8,
		__CFTSDKeyCollatorUCollator = 9,
		__CFTSDKeyRunLoop = 10,
		__CFTSDKeyRunLoopCntr = 11,
   		__CFTSDKeyMachMessageBoost = 12, // valid only in the context of a CFMachPort callout
   		__CFTSDKeyMachMessageHasVoucher = 13,
		// autorelease pool stuff must be higher than run loop constants
		__CFTSDKeyAutoreleaseData2 = 61,
		__CFTSDKeyAutoreleaseData1 = 62,
		__CFTSDKeyExceptionData = 63,
};

複製程式碼

註釋裡有一句 autorelease pool stuff must be higher than run loop constants 的說明,這一點就其實關係到 Run Loop 和 autorelease pool 釋放的順序了。

OSAtomicCompareAndSwapPtrBarrier

/*! @abstract Compare and swap for <code>int</code> values.
    @discussion
	This function compares the value in <code>__oldValue</code> to the value
	in the memory location referenced by <code>__theValue</code>.  If the values
	match, this function stores the value from <code>__newValue</code> into
	that memory location atomically.

	This function is equivalent to {@link OSAtomicCompareAndSwap32}.
    @result Returns TRUE on a match, FALSE otherwise.
 */
OSATOMIC_DEPRECATED_REPLACE_WITH(atomic_compare_exchange_strong)
__OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0)
bool	OSAtomicCompareAndSwapInt( int __oldValue, int __newValue, volatile int *__theValue );

複製程式碼

這一個函式,它首先對 oldValue , theValue 進行比較.

如果兩個值相等,就執行 theValue = newValue,並返回 YES.

如果兩個值不等,返回 NO .

值得注意的是,這個函式引入了 barrier,它的操作是原子的。 這是一個典型的 CAS 操作,無獨有偶,在 RAC 中的一個特點也是使用 Atomic Operations ,完成執行緒同步。

它在CFRunLoopGet0的作用是 : 比較 CFRunLoops 是否為 null 。 如果為 null (第一次建立)了,就把 dict 賦值給 CFRunLoops 。如果不為 null,就釋放掉 dict 。

memory barrier

這裡再稍微提一下 barrier , 上面說它保證了原子操作。

memory barrier 在維基的定義是:

記憶體屏障(英語:Memory barrier),也稱記憶體柵欄,記憶體柵障,屏障指令等,是一類同步屏障指令,是CPU或編譯器在對記憶體隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執行後才可以開始執行此點之後的操作。 大多數現代計算機為了提高效能而採取亂序執行,這使得記憶體屏障成為必須。

而對於 Objective C 的實現來說,幾乎所有的加鎖操作最後都會設定 memory barrier ,官方文件的解釋:

Note: Most types of locks also incorporate a memory barrier to ensure that any preceding load and store instructions are completed before entering the critical section.

為了防止編譯器對我們的程式碼做優化,改變我們程式碼的指令順序,可以採用 barrier 設定對我們的程式碼順序做保證。

CFRunLoopCreate

講完 Run Loop 怎麼獲取,再看 Run Loop 怎麼建立。

對於 CFRunLoopCreate :

static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
    CFRunLoopRef loop = NULL;
    CFRunLoopModeRef rlm;
    uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
    loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL);
    if (NULL == loop) {
	return NULL;
    }
    (void)__CFRunLoopPushPerRunData(loop);
    __CFRunLoopLockInit(&loop->_lock);
    loop->_wakeUpPort = __CFPortAllocate();
    if (CFPORT_NULL == loop->_wakeUpPort) HALT;
    __CFRunLoopSetIgnoreWakeUps(loop);
    loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
    loop->_commonModeItems = NULL;
    loop->_currentMode = NULL;
    loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    loop->_blocks_head = NULL;
    loop->_blocks_tail = NULL;
    loop->_counterpart = NULL;
    loop->_pthread = t;
#if DEPLOYMENT_TARGET_WINDOWS
    loop->_winthread = GetCurrentThreadId();
#else
    loop->_winthread = 0;
#endif
    rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
    if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
    return loop;
}

複製程式碼

大體說一下建立的流程:

  1. 通過 CFRuntimeCreateInstance ,建立一個 CFRunLoopRef 例項。
  2. 對 loop 做初始化設定,比如喚醒埠,commonModes 等的設定。

注意: CFRunLoopPushPerRunData 會在建立時做一些初始化設定, __CFPortAllocate() 會設定喚醒的埠, CFRunLoopSetIgnoreWakeUps 呼叫的原因時,目前處於喚醒狀態,對它的訊息做忽略。 HALT 命令可以停止系統執行,假如 wakeUpPort 為 CFPORT_NULL

CFRuntimeCreateInstance

真正建立得到 CFRunLoopRef 型別的 loop ,呼叫的是 CFRuntimeCreateInstance 來建立的。

它是一個用來建立 CF 例項型別的函式:

CF_EXPORT CFTypeRef _CFRuntimeCreateInstance(CFAllocatorRef allocator, CFTypeID typeID, CFIndex extraBytes, unsigned char *category);
複製程式碼

更具體的解釋可以檢視 CFRuntime.h 對它的定義。

CFRunLoopCreate 給它傳入了一個預設的分配器 kCFAllocatorSystemDefault ,一個 CFRunLoopGetTypeID() ,一個 size 。

CFRunLoopGetTypeID() 的操作如下:

CFTypeID CFRunLoopGetTypeID(void) {
    static dispatch_once_t initOnce;
    dispatch_once(&initOnce, ^{ 
    __kCFRunLoopTypeID = _CFRuntimeRegisterClass(&__CFRunLoopClass); 
    __kCFRunLoopModeTypeID = _CFRuntimeRegisterClass(&__CFRunLoopModeClass); 
    });
    return __kCFRunLoopTypeID;
}

複製程式碼

它在裡面註冊了 CFRunLoopClass 和 CFRunLoopModeClass 的型別,並用返回值,給對應的 typeID 賦值。作為單例,只執行一次。

size 的計算為 :

uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
複製程式碼

size 是一個 CFRunLoop 型別本身的大小,減掉 CFRuntimeBase 型別的大小得到的結果。

為什麼要減去一個 CFRuntimeBase 的型別大小?

檢視 CFRuntime.c 對原始碼,發現裡面會把減掉的 sizeof(CFRuntimeBase) 再給加回來:

CFIndex size = sizeof(CFRuntimeBase) + extraBytes + (usesSystemDefaultAllocator ? 0 : sizeof(CFAllocatorRef));

複製程式碼

CFRunLoopFindMode

static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create) 
複製程式碼

CFRunLoopFindMode 是一個用來查詢 mode 的函式,同時也可以來建立 mode 。

它其中有利用兩個巨集,來對 timer 的種類進行判斷.查閱了一下定義:

#if DEPLOYMENT_TARGET_MACOSX
#define USE_DISPATCH_SOURCE_FOR_TIMERS 1
#define USE_MK_TIMER_TOO 1
#else
#define USE_DISPATCH_SOURCE_FOR_TIMERS 0
#define USE_MK_TIMER_TOO 1
#endif
複製程式碼

也就是說,在 MACOSX 下,同時還會有使用 dispatch timer 來做定時器。而 MK_TIMER 是兩個平臺下都有的。

函式的大體邏輯是先判斷有無,有就返回. 沒有的話,就根據 create 的值決定是否新建立一個 mode .

在 CFRunLoopCreate 裡面,呼叫的程式碼是

 CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true) 
複製程式碼

它會新建立一個 mode 返回。

執行 Run Loop

啟動 Run Loop 有 2 個函式,一個是 CFRunLoopRun , 一個是 CFRunLoopRunInMode

  • DefaultMode 啟動

void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
複製程式碼

注:1.0e10,這個表示1.0乘以10的10次方,這個引數主要是規定RunLoop的時間,傳這個時間,表示執行緒常駐。

主執行緒的RunLoop呼叫函式,就是使用了 CFRunLoopRun

  • 指定 Mode 啟動,允許設定RunLoop超時時間

int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
複製程式碼

檢視上面兩個啟動 Run Loop 執行的函式實現,發現都使用了 CFRunLoopRunSpecific .

CFRunLoopRunSpecific

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    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);
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;

	if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
	if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopPopPerRunData(rl, previousPerRun);
	rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

複製程式碼

1.通過 runloop 的 modeName 查詢當前 mode。因為 CFRunLoopFindMode 的 create 引數為 false , 如果沒找到,直接為 null ,不會建立新的 mode.

2.如果當前 mode 為空,函式結束,返回 CFRunLoopRunFinished .

這裡比較奇怪的是 Boolean did = false 直接寫死了 did 的值,後面又是 return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished . 懷疑 did 的值,應該還有一段程式碼是決定kCFRunLoopRunHandledSource的結果,被蘋果隱藏了沒有開源出來。

3.如果當前 mode 存在,做一些賦值操作 .

4.向觀察者傳送 kCFRunLoopEntry 的訊息,即將進入 RunLoop .

5.進入 CFRunLoopRun 函式,在這裡做一系列觀察和操作。

6.向觀察者傳送 kCFRunLoopExit 的訊息,即將退出 RunLoop .

關於 CFRunLoopRun 在執行的時候,有 4 個列舉值表示它的狀態:

/* Reasons for CFRunLoopRunInMode() to Return */
enum {
    kCFRunLoopRunFinished = 1,//結束
    kCFRunLoopRunStopped = 2,//暫停
    kCFRunLoopRunTimedOut = 3,//超時
    kCFRunLoopRunHandledSource = 4 //執行事件
};

複製程式碼

CFRunLoopDoObservers

CFRunLoopDoObservers 的官方文件說明如下:

A CFRunLoopObserver provides a general means to receive callbacks at different points within a running run loop. In contrast to sources, which fire when an asynchronous event occurs, and timers, which fire when a particular time passes, observers fire at special locations within the execution of the run loop, such as before sources are processed or before the run loop goes to sleep, waiting for an event to occur. Observers can be either one-time events or repeated every time through the run loop’s loop.

Each run loop observer can be registered in only one run loop at a time, although it can be added to multiple run loop modes within that run loop.

一個 CFRunLoopObserver 提供了一個通用的方法,在不同的時機去接受執行中的 runloop 的回撥。與在一個非同步事件發生時觸發的源,和在特定時間之後觸發的定時器相比,在 run loop 執行的過程中, 觀察者會在特定的位置傳送訊號,例如 sources 執行之前活著 run loop 將要休眠之前,等待事件的發生. 觀察者可以是一次性的,或者在通過每次 run loop 的迴圈裡重複。

每個 run loop 觀察者只能在 run loop 中註冊一次,儘管它可以新增到該 run loop 內的多個 run loop mode 中。

CFRunLoopRun

這裡其實有兩個 CFRunLoopRun 函式,一個是暴露給我們在外面使用的,不帶引數的:

CF_EXPORT void CFRunLoopRun(void);
複製程式碼

現在要說的,是這一個:

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) __attribute__((noinline));
複製程式碼

因為函式比較長,所以分段來進行講解.

1.runloop 狀態判斷 / GCD 佇列的埠設定:

    //獲取開始時間
    uint64_t startTSR = mach_absolute_time();

    //對 runloop 狀態做判斷,檢查是否處於 stop 的情況
    if (__CFRunLoopIsStopped(rl)) {
           __CFRunLoopUnsetStopped(rl);
	       return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
	       rlm->_stopped = false;
	       return kCFRunLoopRunStopped;
    }
    
    //宣告 dispatchPort 變數,作為一個 mach_port 通訊的埠,初始化值為 MACH_PORT_NULL
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    
    // 檢測是否在主執行緒 && ( (是佇列發的訊息&&mode為null)||(不是佇列發的訊息&&不在主佇列))
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
    
    //如果是佇列安全的,並且是主執行緒runloop,設定它對應的通訊埠
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
複製程式碼
  • HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY
#define HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY 0
複製程式碼

這裡對於它的定義是 0 ,寫死了。我猜測應該還是有一個函式去做判斷的。

目前只能從字面意思猜測,代表是否只分發 dispatch 訊息的

  • _dispatch_get_main_queue_port_4CF
#define _dispatch_get_main_queue_port_4CF _dispatch_get_main_queue_handle_4CF
複製程式碼

它是 dispatch_get_main_queue_handle_4CF 的巨集,存在 libdispatch 中,裡面對它的實現為:

dispatch_runloop_handle_t
_dispatch_get_main_queue_handle_4CF(void)
{
	dispatch_queue_t dq = &_dispatch_main_q;
	dispatch_once_f(&_dispatch_main_q_handle_pred, dq,
			_dispatch_runloop_queue_handle_init);
	return _dispatch_runloop_queue_get_handle(dq);
}

複製程式碼

返回的是主執行緒 runloop 所關聯的的埠。

2.MACOSX 下,宣告一個 mode 的佇列通訊埠(在 MACOSX 環境中):

#if USE_DISPATCH_SOURCE_FOR_TIMERS
    mach_port_name_t modeQueuePort = MACH_PORT_NULL;
    if (rlm->_queue) {
        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
複製程式碼

3.根據超時 seconds 的時長,做對應操作。

    dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    //小於等於 0 ,片刻的超時(instant timeout),直接設定為 0 ,不超時
    if (seconds <= 0.0) { 
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    } else if (seconds <= TIMER_INTERVAL_LIMIT) {//在限制的超時間隔內
    //根據是否為主執行緒,設定佇列是主佇列還是後臺佇列
	dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
	//建立一個 GCD Timer
	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);
	//把 timer 和 context 給關聯起來
	dispatch_set_context(timeout_timer, timeout_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);
   //恢復喚起 timer 執行
   dispatch_resume(timeout_timer);
    } else { 
    	 //無限期超時
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }
複製程式碼

4.進入 do - while 迴圈,直到 reVal 不為 0 。以下程式碼為更好理解,刪去 windows 相關:

    // 設定判斷是否為最後一次 dispatch 的埠通訊的變數
    Boolean didDispatchPortLastTime = true;
    // 設定一個結果變數,最後為幾個 CFRunLoopRunInMode 裡返回狀態之一。
    int32_t retVal = 0;
    do {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        // 一個狀態變數,用於 訊息狀態 標誌,初始值為 UNCHAMGED
        voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED;
        voucher_t voucherCopy = NULL;
#endif
		 //宣告一個 msg_buffer 陣列
        uint8_t msg_buffer[3 * 1024];
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
		 //宣告和 mach port 有關的 port 和 msg 變數
        mach_msg_header_t *msg = NULL;
        mach_port_t livePort = MACH_PORT_NULL;

		 // 宣告一個型別為 CFPortSet 的 waitSet, 值為 run loop mode 裡的 portSet.
		 __CFPortSet waitSet = rlm->_portSet;

 		//將 run loop 從忽略喚醒訊息的狀態 unset ,開始接受喚醒訊息
        __CFRunLoopUnsetIgnoreWakeUps(rl);
        
		 // 2. 通知 observers , Run Loop 即將觸發 Timer 回撥。
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        // 3. 通知 observers , Run Loop 即將觸發 Source0 (非 mach port) 回撥。
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
		 // 執行 block
	     __CFRunLoopDoBlocks(rl, rlm);
		//  4. 執行 Source0 (非 mach port) 。
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {// 執行完 source0 後,假如還有需要執行的,再執行一次 block
            __CFRunLoopDoBlocks(rl, rlm);
		}

		// poll 變數,是否處理 source 或未超時
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
	
        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. 如果有 Source1 (基於port) 處於 ready 狀態
            //    直接處理這個 Source1 然後跳轉去處理訊息(handle_msg)。
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                goto handle_msg;
            }

        }


    didDispatchPortLastTime = false;
    
	// 6.通知 Observers: RunLoop 的執行緒即將進入休眠(sleep)。
	// 注意到如果實際處理了 source0 或者超時,不會進入睡眠,所以不會通知
	if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
	
	// 設定標誌位, Run Loop 休眠
	__CFRunLoopSetSleeping(rl);

   // 使用 GCD 的話,將 GCD 埠加入監聽埠集合中
   __CFPortSetInsert(dispatchPort, waitSet);
        
	__CFRunLoopModeUnlock(rlm);
	__CFRunLoopUnlock(rl);
	// 休眠開始的時間,根據 poll 狀態決定為 0 或者當前的絕對時間
   CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();

// 7. 通過 CFRunLoopServiceMachPort 呼叫 mach_msg 休眠,等待被 mach_msg 訊息喚醒

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
// 如果在 MACOSX 中
#if USE_DISPATCH_SOURCE_FOR_TIMERS
		// 處理 GCD timer 
        do {
            if (kCFUseCollectableAllocator) {//假如有kCFUseCollectableAllocator分配器,使用 memset 清空msg_buffer
                // objc_clear_stack(0);
                // <rdar://problem/16393959>
                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;
            
            // 設定 mach port 通訊,會睡眠執行緒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
            
            // modeQueue 存在,而且為 livePort
            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.
                //執行 run loop mode 裡的佇列,直到佇列都執行完成
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                
                if (rlm->_timerFired) {//假如 _timerFired 為真,把 livePort 作為佇列埠,在之前服務於 timers
                    rlm->_timerFired = false;
                    //退出
                    break;
                } else {// _timerFired 為假, 並且 msg 存在不為 msg_buffer, 釋放 msg
                    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                }
            } else {
                //退出
                break;
            }
        } while (1);
        
#else // 不在 MACOSX 中
        if (kCFUseCollectableAllocator) {//如果 kCFUseCollectableAllocator 分配器,使用 memset 清空 msg_buffer
            // objc_clear_stack(0);
            // <rdar://problem/16393959>
            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;
        //CFRunLoopServiceMachPort 會讓執行緒休眠
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
#endif
        
        
        //上鎖
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
        
		// 根據 poll 的值,記錄休眠時間
        rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));

		//對 waitSet 裡的 dispatchPort 埠做移除
        __CFPortSetRemove(dispatchPort, waitSet);
        
        //讓 Run Loop 忽略喚醒訊息,因為已經重新在執行了
        __CFRunLoopSetIgnoreWakeUps(rl);

        // user callouts now OK again
		__CFRunLoopUnsetSleeping(rl);
		
	     // 8. 通知 observers: kCFRunLoopAfterWaiting, 執行緒剛被喚醒
        // 注意實際處理過 source 0 或者已經超時的話,不會通知(因為沒有睡)
		if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
		
		//處理對應喚醒的訊息
        handle_msg:;
        
        //將 Run Loop 重新忽略喚醒訊息,因為已經重新在執行了
        __CFRunLoopSetIgnoreWakeUps(rl);


        if (MACH_PORT_NULL == livePort) {// livePort 為空,什麼事都不做
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
        } else if (livePort == rl->_wakeUpPort) {// livePort 等於 run loop 的 _wakeUpPort
            // 被 CFRunLoopWakeUp 函式喚醒的
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();

        }
        
// 在 MACOSX 裡
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {//livePort 等於 modeQueuePort
        	 //9.1-1 被 timers 喚醒,處理 timers
            CFRUNLOOP_WAKEUP_FOR_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) {//livePort 等於 run loop mode 的 _timerPort
        	 // 9.1-2 被 timers 喚醒,處理 timers
            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
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
        else if (livePort == dispatchPort) {// livePort 等於 dispatchPort
        	 // 9.2 如果有dispatch到main_queue的block,執行block
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            
            //解鎖
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            
            //設定 CFTSDKeyIsInGCDMainQ 位置的 TSD 為 6 .
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
			
			// 處理 msg
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            
            //設定 CFTSDKeyIsInGCDMainQ 位置的 TSD 為 0.
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            
            //上鎖
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            
            //設定變數
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
            //9.3 被 source (基於 mach port) 喚醒
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
            
            // 假如我們 從這個 mach_msg 中接收到一個 voucher,然後在 TSD 中放置一個複製的新的 voucher.
            // CFMachPortBoost 會在 TSD 中去查詢這個 voucher. 
            // 通過使用 TSD 中的值,我們將 CFMachPortBoost 繫結到這個接收到的 mach_msg 中,在這兩段程式碼之間沒有任何機會再次設定憑證
            voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);

            // Despite the name, this works for windows handles as well
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            
            if (rls) {//如果 rls 存在
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
				mach_msg_header_t *reply = NULL;
				//處理 Source ,並返回執行結果
				sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
				if (NULL != reply) {//傳送reply訊息(假如 reply 不為空)
		    		(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
		    		//釋放 reply 變數
		    		CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
				}

	    	}
            
            // Restore the previous voucher
            _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
            
        } 
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif

   // 執行加入到Loop的block
	__CFRunLoopDoBlocks(rl, rlm);
        
        
	//根據一次迴圈後的狀態,給 retVal 賦值 。狀態不變則繼續迴圈
	if (sourceHandledThisLoop && stopAfterHandle) {
	    retVal = kCFRunLoopRunHandledSource;
	    
   } else if (timeout_context->termTSR < mach_absolute_time()) {
        retVal = kCFRunLoopRunTimedOut;
        
	} else if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
	    retVal = kCFRunLoopRunStopped;
	    
	} else if (rlm->_stopped) {
	    rlm->_stopped = false;
	    retVal = kCFRunLoopRunStopped;
	} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
	    retVal = kCFRunLoopRunFinished;
	}
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
		// 迴圈一次後收尾處理
        voucher_mach_msg_revert(voucherState);
        os_release(voucherCopy);
#endif

    } while (0 == retVal);

複製程式碼

上面的程式碼,有幾個地方的定義可能需要結合其它地方才能理解:

  • voucher_mach_msg_state_t 在 mach.h 中:
/*!
* @typedef voucher_mach_msg_state_t
*
* @abstract
* Opaque object encapsulating state changed by voucher_mach_msg_adopt().
*/
typedef struct voucher_mach_msg_state_s *voucher_mach_msg_state_t;
複製程式碼

不透明的物件封裝狀態由 voucher_mach_msg_adopt() 改變,它代表一種 mach_msg 通訊時的狀態。

  • HANDLE:
DISPATCH_EXPORT HANDLE _dispatch_get_main_queue_handle_4CF(void);
複製程式碼

返回作為主佇列相關聯的 run loop 。

  • memset :作用是在一段記憶體塊中填充某個給定的值,它是對較大的結構體或陣列進行清零操作的一種最快方法。

5.釋放 timerout_timer 定時器相關

    if (timeout_timer) {//如果存在,取消並釋放
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {//不存在,將對應的 timeour_context 釋放
        free(timeout_context);
    }
    //結束返回 retVal 狀態。
    return retVal;
複製程式碼

CFRunLoopServiceMachPort

這個函式是讓執行緒休眠的關鍵,它在裡面做了和 mach port 相關的操作。

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, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
    Boolean originalBuffer = true;
    kern_return_t ret = KERN_SUCCESS;
    for (;;) {		/* In that sleep of death what nightmares may come ... */
    
        // msg 相關資料設定
        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;
        
        // 根據 timeout 的值,決定 Run Loop 是休眠還是執行
        // timeout  為 TIMEOUT_INFINITY 時,才執行 CFRUNLOOP_SLEEP() 休眠
        if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
        
        // 傳送並接收 mach port 訊息
        ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|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);

      
        // 在 mach_msg 之後注意所有 voucher 相關的正常執行
        // 假如我們沒有釋放前面的 voucher , 將會出現記憶體洩漏
        voucher_mach_msg_revert(*voucherState);
        
        // 會有呼叫者去負責呼叫 voucher_mach_msg_revert .它會讓接收到的 voucher 變成當前的這一個值。
        *voucherState = voucher_mach_msg_adopt(msg);
        
        if (voucherCopy) {
            if (*voucherState != VOUCHER_MACH_MSG_STATE_UNCHANGED) {
 
                // 呼叫者 在這裡 請求了一個 voucher 的複製的值。通過在 mach_msg 前後做這個操作,我們確保在 mach_msg 返回和使用 voucher 的複製值的時候,沒有涉及設定 voucher 的值。
                // 為確保  CFMachPortBoost 使用的  voucher ,所以我們只在 voucher 不是  state_unchanged 的時候,去設定 TSD 。
                *voucherCopy = voucher_copy();
            } else {
            	//值為 VOUCHER_MACH_MSG_STATE_UNCHANGED ,置為 null
                *voucherCopy = NULL;
            }
        }

	    // 喚醒 Run Loop
        CFRUNLOOP_WAKEUP(ret);
        
        if (MACH_MSG_SUCCESS == ret) {// ret 成功後,設定 livePort 的值,返回 true
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            return true;
        }
        if (MACH_RCV_TIMED_OUT == ret) {// ret 超時,釋放 msg ,有關變數置空,返回 false
            if (!originalBuffer) free(msg);
            *buffer = NULL;
            *livePort = MACH_PORT_NULL;
            return false;
        }
        //ret 不為 MACH_RCV_TOO_LARGE ,退出迴圈
        if (MACH_RCV_TOO_LARGE != ret) break;
        //ret 為 MACH_RCV_TOO_LARGE,做釋放操作,重新進入迴圈
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
        if (originalBuffer) *buffer = NULL;
        originalBuffer = false;
        *buffer = realloc(*buffer, buffer_size);
    }
    HALT;
    return false;
}

複製程式碼

這裡有查詢 CFRUNLOOP_SLEEP() 和 CFRUNLOOP_POLL() 等函式,都是 do { } while (0) 這樣的巨集,沒有真正實現程式碼,所以無法再看到具體的情況。

總結

這一次學習的過程,最大的感觸,就是對於知識的相通性。

例如對於 TSD 執行緒私有資料的理解,搜尋很多跟 iOS 有關資料都找不到說明,最後是在 unix 相關的文章才看到解釋。還有 Event Loop 的機制在其它平臺等實現。

當然整個過程比較枯燥,閱讀的量也比較大,需要耐心。

比較遺憾的是,有一些地方,蘋果並沒有給出具體的程式碼實現或者明確的解釋。

本人水平有限,如有錯誤和值得商榷的地方,歡迎大家拍磚。

參考

前端思維轉變--從事件驅動到資料驅動

蘋果文件--RunLoop

CFRunLoop.c

CFPlatform.c

深入理解RunLoop

XNU 原始碼

執行緒私有資料

UNIX環境高階程式設計——執行緒私有資料

相關文章