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!