1. 程式人生 > >RunLoop總結:RunLoop 與GCD 、Autorelease Pool之間的關係

RunLoop總結:RunLoop 與GCD 、Autorelease Pool之間的關係

如果在面試中問到RunLoop相關的知識,很有可能也會問到RunLoop與GCD、Autorelease Pool有沒有關係,哪些地方用到了GCD、Autorelease Pool等。
So,本文就總結一下RunLoop與GCD和 Autorelease Pool 之間的關係,看看在RunLoop實現中,哪些地方間接或者直接使用、操作到了GCD 和Autorelease Pool。

RunLoop 與GCD 的關係

在RunLoop 中大量使用到了GCD,首先來看一下 CFRrunLoop.c 中引入的其他標頭檔案。

#include <CoreFoundation/CFRunLoop.h>
#include <CoreFoundation/CFSet.h> #include <CoreFoundation/CFBag.h> #include <CoreFoundation/CFNumber.h> #include <CoreFoundation/CFPreferences.h> #include "CFInternal.h" #include <math.h> #include <stdio.h> #include <limits.h> #include <pthread.h> #include <dispatch/dispatch.h> // GCD 庫
······

然後,如果我們在RunLoop中搜索一下 dispatch,可以搜尋出來 130個結果。
接下來,我們來看看RunLoop的主要實現邏輯中哪些地方用到的 GCD。

1.RunLoop 的超時時間

我們在前面介紹過RunLoop 啟動在 CoreFoudation 庫中有兩個API:

//mode預設為defaultMode、超時時間是100億秒、false
void CFRunLoopRun(void)
// 可以設定mode、runloop 超時時間、是否處理完source立刻返回
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean
returnAfterSourceHandled)

而RunLoop 的超時時間就是使用 GCD 中的 dispatch_source_t來實現的,摘自 __CFRunLoopRun中的原始碼:

    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;
    } else if (seconds <= TIMER_INTERVAL_LIMIT) { //超時時間在最大限制內,才建立timeout_timer
        dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
        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;
    }

如果看不懂這段原始碼,可以先去看看GCD API 記錄 (三)中的 dispatch_source中的timer

2.執行GCD MainQueue 上的非同步任務

__CFRunLoopRun方法的前幾行,有一個變數dispatchPort,它的作用是儲存Main_Queue的port,便於後面RunLoop拿到GCD 主執行緒中的非同步任務來執行。


mach_port_name_t dispatchPort = MACH_PORT_NULL;

······
// 只有在MainRunLoop,才會有下面這行賦值,否則 dispatchPort 為NULL
dispatchPort = _dispatch_get_main_queue_port_4CF();

來看一下,RunLoop 是如何執行GCD中MainQueue上的任務的:

// 中間去掉了一些巨集判斷相關的邏輯程式碼
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
    msg = (mach_msg_header_t *)msg_buffer;
    if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
        goto handle_msg;
    }
}
didDispatchPortLastTime = false;

看來,關鍵的邏輯都在 handle_msg中。

handle_msg 中的程式碼片段:

......
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
    // 獲取GCDMainQ上的非同步任務並執行
    __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
    _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
    __CFRunLoopLock(rl);
    __CFRunLoopModeLock(rlm);
    sourceHandledThisLoop = true;
    didDispatchPortLastTime = true;
}
......   

static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(void *msg) {
    _dispatch_main_queue_callback_4CF(msg);
    asm __volatile__(""); // thwart tail-call optimization
}

從上面的原始碼片段可以看出,有判斷是否是在MainRunLoop,有獲取Main_Queue 的port,並且有呼叫 Main_Queue 上的回撥,這隻能是是 GCD 主佇列上的非同步任務。即:dispatch_async(dispatch_get_main_queue(), block)產生的任務。

RunLoop 與 Autorelease Pool的關係

RunLoop與 Autorelease Pool 有關係麼?
有。
我們總是看到有文章說程式啟動後,蘋果在主執行緒 RunLoop 裡註冊了兩個 Observer:
第一個 Observer 監視的事件是 Entry(即將進入Loop),其回撥內會呼叫 _objc_autoreleasePoolPush() 建立自動釋放池。其 order 是-2147483647,優先順序最高,保證建立釋放池發生在其他所有回撥之前。
第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入睡眠) 和 Exit(即將退出Loop),
BeforeWaiting(準備進入睡眠)時呼叫_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並建立新池;
Exit(即將退出Loop) 時呼叫 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先順序最低,保證其釋放池子發生在其他所有回撥之後。

打印出MainRunLoop,可以看到MainRunLoop的 Common mode Items 中就有這兩個觀察者

由 Activity 的列舉值

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

activities = 0x1,對應的就是kCFRunLoopEntry;
activities = 0xa0,對應的就是kCFRunLoopBeforeWaiting | kCFRunLoopExit 。

可能很多人看了上面的結論和Log 資訊,都有這樣的疑惑:_wrapRunLoopWithAutoreleasePoolHandler()內部是如何處理自動釋放池的?你說它釋放了舊的 AutoreleasePool,並新建了一個新的,就是這樣?
目前,我也不知道如何檢視 _wrapRunLoopWithAutoreleasePoolHandler() 中的實現,如果你有方式獲取到她的內部資訊,或者呼叫堆疊,歡迎告知我!

AutoreleasePool原理擴充套件

這一小節,全部摘自黑幕背後的Autorelease,你可以閱讀原文,瞭解更多 Autorelease 內容。
ARC下,我們使用@autoreleasepool{}來使用一個AutoreleasePool,隨後編譯器將其改寫成下面的樣子:

void *context = objc_autoreleasePoolPush();
// {}中的程式碼
objc_autoreleasePoolPop(context);

而這兩個函式都是對AutoreleasePoolPage的簡單封裝,所以自動釋放機制的核心就在於這個類。

AutoreleasePoolPage是一個C++實現的類

  • AutoreleasePool並沒有單獨的結構,而是由若干個AutoreleasePoolPage以雙向連結串列的形式組合而成(分別對應結構中的parent指標和child指標)
  • AutoreleasePool是按執行緒一一對應的(結構中的thread指標指向當前執行緒)
  • AutoreleasePoolPage每個物件會開闢4096位元組記憶體(也就是虛擬記憶體一頁的大小),除了上面的例項變數所佔空間,剩下的空間全部用來儲存autorelease物件的地址
  • 上面的id *next指標作為遊標指向棧頂最新add進來的autorelease物件的下一個位置
  • 一個AutoreleasePoolPage的空間被佔滿時,會新建一個AutoreleasePoolPage物件,連線連結串列,後來的autorelease物件在新的page加入

所以,若當前執行緒中只有一個AutoreleasePoolPage物件,並記錄了很多autorelease物件地址時記憶體如下圖:

圖中的情況,這一頁再加入一個autorelease物件就要滿了(也就是next指標馬上指向棧頂),這時就要執行上面說的操作,建立下一頁page物件,與這一頁連結串列連線完成後,新page的next指標被初始化在棧底(begin的位置),然後繼續向棧頂新增新物件。

所以,向一個物件傳送- autorelease訊息,就是將這個物件加入到當前AutoreleasePoolPage的棧頂next指標指向的位置。

* AutoreleasePool釋放*
每當進行一次objc_autoreleasePoolPush呼叫時,runtime向當前的AutoreleasePoolPage中add進一個哨兵物件,值為0(也就是個nil),那麼這一個page就變成了下面的樣子:

objc_autoreleasePoolPush的返回值正是這個哨兵物件的地址,被objc_autoreleasePoolPop(哨兵物件)作為入參,於是:

  • 1.根據傳入的哨兵物件地址找到哨兵物件所處的page
  • 2.在當前page中,將晚於哨兵物件插入的所有autorelease物件都發送一次- release訊息,並向回移動next指標到正確位置
  • 3.補充2:從最新加入的物件一直向前清理,可以向前跨越若干個page,直到哨兵所在的page

剛才的objc_autoreleasePoolPop執行後,最終變成了下面的樣子:

Have Fun!