重拾RunLoop之原始碼分析1

NeroXie發表於2019-05-14

原文連結重拾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;
}

複製程式碼

__CFTSDTabledata陣列用來儲存私有資料,destructors陣列用來儲存釋放函式(後面也會提到)。destructorCount記錄destructors陣列元素的個數。

_CFGetTSD的作用就是獲取__CFTSDTabledata資料,並返回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;
}
複製程式碼

通過原始碼我們可以知道:

  1. RunLoop和執行緒之間是一一對應的,它們之間的關係儲存在一個全域性字典以及執行緒私有資料中。
  2. 線上程建立的時候,是沒有對應的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進入後首先判斷pInstNULL,那麼new一個物件,然後解鎖返回物件。喚醒B,這是B進入發現第二個判斷通過不了(因為pInst已經有值了),這樣的話B就直接解鎖返回物件。假設只有最外層的判斷的話,那麼B也會建立一個物件。

我想這裡應該也是類似的作用吧。

第二個就是RunLoop銷燬的時機,這個會在RunLoop的釋放說明。

RunLoop的建立

使用__CFRunLoopCreate返回一個CFRunLoopRef的例項,這個函式大致分為兩步:

  1. 使用_CFRuntimeCreateInstance建立一個CFRunLoopRef例項,其實現為CFRuntime.c檔案;
  2. 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會迴圈遍歷datadestructors的資料,並且把old變數作為destructors裡函式的引數。所以當執行緒退出的時候,會呼叫到RunLoop的解構函式__CFFinalizeRunLoop釋放RunLoop。

RunLoop執行

RunLoop通過CFRunLoopRunCFRunLoopRunInMode這兩個函式執行。

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_run

參考

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

相關文章