重拾RunLoop之原始碼分析1
原文連結 重拾RunLoop之原始碼分析1
雖然自己很早前就看過RunLoop的原始碼,當時看得時候,有點地方還是比較生澀的。所有抽了個時間,重新整理了一下之前RunLoop的筆記。CoreFoundation原始碼關於RunLoop的原始碼主要集中在 CFRunLoop.c
檔案中。
RunLoop的獲取
蘋果並不允許我們直接建立RunLoop,RunLoop的建立在第一次獲取的時候,使用 [NSRunLoop mainRunLoop]
或 CFRunLoopGetMain()
可以獲取主執行緒的RunLoop;通過 [NSRunLoop currentRunLoop]
或 CFRunLoopGetCurrent()
獲取當前執行緒的RunLoop。
CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
的原始碼如下:
// 主執行緒的RunLoop CFRunLoopRef CFRunLoopGetMain(void) { CHECK_FOR_FORK();//判斷是否需要fork 程序 static CFRunLoopRef __main = NULL; // no retain needed if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed return __main; } // 當前執行緒的RunLoop CFRunLoopRef CFRunLoopGetCurrent(void) { CHECK_FOR_FORK(); //先從TSD中查詢有沒有相關的runloop資訊,有則返回。 //我們可以理解為runloop不光存在與全域性字典中,也存在中TSD中。 CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop); if (rl) return rl; return _CFRunLoopGet0(pthread_self()); } 複製程式碼
CHECK_FOR_FORK();
用來判斷是否需要fork程序,這裡我們可以暫時不管。
在獲取主執行緒RunLoop的時候,它使用了 static CFRunLoopRef __main
進行儲存,當第二次呼叫 CFRunLoopGetMain()
, __main
是有值的,就不會再重新建立,否則就使用 _CFRunLoopGet0
進行建立,傳入的是 pthread_main_thread_np()
即主執行緒。
在獲取當前執行緒的RunLoop的時候,首頁會通過 _CFGetTSD
獲取RunLoop,如果沒有再通過 _CFRunLoopGet0
,傳入的是當前的執行緒。
Thread-specific data
Thread-specific data
是執行緒私有資料就是上面的 TSD
,顧名思義就是存一些特定的資料的,RunLoop會儲存線上程的私有資料裡。
// __CFTSDTable typedef struct __CFTSDTable { uint32_t destructorCount; uintptr_t data[CF_TSD_MAX_SLOTS]; tsdDestructor destructors[CF_TSD_MAX_SLOTS]; } __CFTSDTable; // _CFGetTSD CF_EXPORT void *_CFGetTSD(uint32_t slot) { __CFTSDTable *table = __CFTSDGetTable(); if (!table) { return NULL; } uintptr_t *slots = (uintptr_t *)(table->data); return (void *)slots[slot]; } // _CFSetTSD CF_EXPORT void *_CFSetTSD(uint32_t slot, void *newVal, tsdDestructor destructor) { __CFTSDTable *table = __CFTSDGetTable(); if (!table) { return NULL; } void *oldVal = (void *)table->data[slot]; table->data[slot] = (uintptr_t)newVal; table->destructors[slot] = destructor; return oldVal; } 複製程式碼
__CFTSDTable
的 data
陣列用來儲存私有資料, destructors
陣列用來儲存釋放函式(後面也會提到)。 destructorCount
記錄 destructors
陣列元素的個數。
_CFGetTSD
的作用就是獲取 __CFTSDTable
的 data
資料,並返回 slot
的值。
_CFSetTSD
的作用就是給 __CFTSDTable
裡設定 data[slot]
和 destructors[slot]
位置的值。
_CFRunLoopGet0
// t==0 is a synonym for "main thread" that always works // t==0是主執行緒的代名詞 CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { // 當前執行緒為0,則取主執行緒 if (pthread_equal(t, kNilPthreadT)) { t = pthread_main_thread_np(); } __CFLock(&loopsLock); // __CFRunLoops是一個全域性的靜態字典。 // 如果該字典為空,就進行以下兩步操作 // 1.建立一個臨時字典; // 2.建立主執行緒的RunLoop,並將它存到臨時字典裡 // 3.OSAtomicCompareAndSwapPtrBarrier用來將這個臨時字典複製到全域性字典裡; // 並且使用了鎖機制確保安全。 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); } // 當前執行緒RunLoop的獲取,獲取不到就使用__CFRunLoopCreate建立一個RunLoop,並儲存在全域性字典裡 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); } //t為當前執行緒的話,將loop儲存線上程私有資料中 if (pthread_equal(t, pthread_self())) { // _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); // __CFFinalizeRunLoop是RunLoop的解構函式, // PTHREAD_DESTRUCTOR_ITERATIONS 表示是執行緒退出時銷燬執行緒私有資料的最大次數 // 這也是RunLoop的釋放時機--執行緒退出的時候 if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); } } return loop; } 複製程式碼
通過原始碼我們可以知道:
- RunLoop和執行緒之間是一一對應的,它們之間的關係儲存在一個全域性字典以及執行緒私有資料中。
- 線上程建立的時候,是沒有對應的RunLoop,它的建立是在第一次獲取的時候,它的銷燬則發生線上程銷燬的時候。
之前在看原始碼的時候有兩個地方不是很理解。第一個就是為什麼上面的loop要再取一次,在《程式設計師的自我修養》第29頁中得到啟發。裡面關於單例有這樣一段程式碼:
volatile T* pInst = 0; T* GetInstance() { if(pInst == NULL) { lock(); if(pInst == NULL) pInst = new T; unlock(); } return pInst; } 複製程式碼
書上只說明雙重if在這裡可以讓lock的呼叫開銷降到最低。為什麼有這個效果,這裡做一下說明。
在不考慮CPU亂序的情況下,假設有兩個執行緒A、B同時訪問 GetInstance()
,A和B同時執行第一個判斷語句,結果一樣,都進入了程式碼塊。 lock()
的設定就是隻允許一個執行緒進入,假設A先進入,B在等待。A進入後首先判斷 pInst
為 NULL
,那麼new一個物件,然後解鎖返回物件。喚醒B,這是B進入發現第二個判斷通過不了(因為 pInst
已經有值了),這樣的話B就直接解鎖返回物件。假設只有最外層的判斷的話,那麼B也會建立一個物件。
我想這裡應該也是類似的作用吧。
第二個就是RunLoop銷燬的時機,這個會在RunLoop的釋放說明。
RunLoop的建立
使用 __CFRunLoopCreate
返回一個 CFRunLoopRef
的例項,這個函式大致分為兩步:
- 使用
_CFRuntimeCreateInstance
建立一個CFRunLoopRef
例項,其實現為CFRuntime.c
檔案; - 對
CFRunLoopRef
進行初始化配置,包括呼叫__CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
。
在 __CFRunLoopFindMode
裡講到了RunLoop的定時器,用巨集進行了判斷
#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
下,同時還會有使用 GCD Timer
來做定時器,而 MK_TIMER
是兩個平臺下都有的。
RunLoop的釋放
關於RunLoop的釋放是發生線上程銷燬的時候。為什麼這麼說呢? __CFTSDGetTable()
中有一個 __CFTSDFinalize
的解構函式,其實現如下:
static void __CFTSDFinalize(void *arg) { __CFTSDSetSpecific(arg); if (!arg || arg == CF_TSD_BAD_PTR) { return; } __CFTSDTable *table = (__CFTSDTable *)arg; table->destructorCount++; 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); __CFTSDSetSpecific(CF_TSD_BAD_PTR); return; } } 複製程式碼
我們可以看到, table
會迴圈遍歷 data
和 destructors
的資料,並且把 old
變數作為 destructors
裡函式的引數。所以當執行緒退出的時候,會呼叫到RunLoop的解構函式 __CFFinalizeRunLoop
釋放RunLoop。
RunLoop執行
RunLoop通過 CFRunLoopRun
和 CFRunLoopRunInMode
這兩個函式執行。
CFRunLoopRun
void CFRunLoopRun(void) { int32_t result; do { result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); CHECK_FOR_FORK(); } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); } 複製程式碼
函式預設在 kCFRunLoopDefaultMode
下執行RunLoop,並且一直執行在一個do-while的迴圈裡。 另外函式不會主動呼叫 CFRunLoopStop
函式( kCFRunLoopRunStopped
)或者將所有事件源移除( kCFRunLoopRunFinished
)。 從這裡我們也可以瞭解,如果RunLoop的 _currentMode
值變化,只能退出,然後重新指定一個Mode進入。
CFRunLoopRunInMode
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {/* DOES CALLOUT */ CHECK_FOR_FORK(); return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); } 複製程式碼
無論是 CFRunLoopRun
還是 CFRunLoopRunInMode
都是呼叫了 CFRunLoopRunSpecific
。
CFRunLoopRunSpecific
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {/* DOES CALLOUT */ CHECK_FOR_FORK(); if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished; __CFRunLoopLock(rl); // 首先根據modeName找到對應Mode,如果沒有則建立一個新的Mode CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); // 如果mode為空或者mode中沒有相關的source/timer/observer,則不進入迴圈 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; // 1.通知observer即將進入RunLoop if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); // RunLoop真正執行的方法:第2~9步 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; } 複製程式碼
__CFRunLoopRun
__CFRunLoopRun
可以說是RunLoop執行的核心方法。由於程式碼過長,這裡對程式碼進行了刪減,簡化後的程式碼如下:
/* rl, rlm are locked on entrance and exit */ 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 kCFRunLoopRunStopped,不進入迴圈 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(); // USE_DISPATCH_SOURCE_FOR_TIMERS為1表示在MACOSX下,iOS不會呼叫這段程式碼 #if USE_DISPATCH_SOURCE_FOR_TIMERS ... #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) { ... } // 永不超時 else { seconds = 9999999999.0; timeout_context->termTSR = UINT64_MAX; } // 標誌位預設為true Boolean didDispatchPortLastTime = true; // 記錄最後RunLoop的狀態 int32_t retVal = 0; do { ... // 需要監聽的埠 __CFPortSet waitSet = rlm->_portSet; // 設定RunLoop為可以被喚醒狀態 __CFRunLoopUnsetIgnoreWakeUps(rl); // 2.通知observer,即將處理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) { __CFRunLoopDoBlocks(rl, rlm); } // 如果沒有Sources0事件處理並且沒有超時,poll為false 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.接收dispatchPort埠的訊息,(接收source1事件)直接跳到第9步 if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) { goto handle_msg; } #elif DEPLOYMENT_TARGET_WINDOWS if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) { goto handle_msg; } #endif } didDispatchPortLastTime = false; // 6.通知observer,即將進入休眠 if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); // 設定RunLoop為休眠狀態 __CFRunLoopSetSleeping(rl); ... #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI #if USE_DISPATCH_SOURCE_FOR_TIMERS ... #else if (kCFUseCollectableAllocator) { // objc_clear_stack(0); // <rdar://problem/16393959> memset(msg_buffer, 0, sizeof(msg_buffer)); } msg = (mach_msg_header_t *)msg_buffer; // 7.接收waitSet埠的訊息,這些訊息可能是 // 一個基於 port 的Source 的事件。 // 一個 Timer 到時間了 // RunLoop 自身的超時時間到了 // 被其他什麼呼叫者手動喚醒 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); #endif ... // user callouts now OK again //取消runloop的休眠狀態 __CFRunLoopUnsetSleeping(rl); // 8.通知observer,執行緒剛被喚醒 if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); //9.處理收到的訊息,之後重新進入第2步 handle_msg:; ... if (MACH_PORT_NULL == livePort) { CFRUNLOOP_WAKEUP_FOR_NOTHING(); // handle nothing } 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 ... // 這裡是GCD相關的定時器,可以忽略 #endif #if USE_MK_TIMER_TOO // 如果是定時器事件 else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) { CFRUNLOOP_WAKEUP_FOR_TIMER(); // 9.1處理timer事件 if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { // Re-arm the next timer __CFArmNextTimerInMode(rlm, rl); } } #endif ... CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort); // 有source1事件 if (rls) { mach_msg_header_t *reply = NULL; // 9.2 處理source1事件 sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop; } ... if (sourceHandledThisLoop && stopAfterHandle) { // 處理完事件就返回 retVal = kCFRunLoopRunHandledSource; } else if (timeout_context->termTSR < mach_absolute_time()) { // 超時 retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(rl)) { // RunLoop終止 __CFRunLoopUnsetStopped(rl); retVal = kCFRunLoopRunStopped; } else if (rlm->_stopped) { // mode終止 rlm->_stopped = false; retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { retVal = kCFRunLoopRunFinished; } ... } while (0 == retVal); ... return retVal; } 複製程式碼
這裡盜一張RunLoop執行流程的圖:

參考
程式設計師的自我修養深入理解RunLoop 蘋果文件--RunLoop