RunLoop總結(jié):RunLoop 與GCD 、Autorelease Pool之間的關(guān)系

如果在面試中問(wèn)到RunLoop相關(guān)的知識(shí),很有可能也會(huì)問(wèn)到RunLoop與GCD、Autorelease Pool有沒(méi)有關(guān)系,哪些地方用到了GCD、Autorelease Pool等。
So,本文就總結(jié)一下RunLoop與GCD和 Autorelease Pool 之間的關(guān)系,看看在RunLoop實(shí)現(xiàn)中,哪些地方間接或者直接使用、操作到了GCD 和Autorelease Pool。

RunLoop 與GCD 的關(guān)系
在RunLoop 中大量使用到了GCD,首先來(lái)看一下 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 庫(kù)

······
1
2
3
4
5
6
7
8
9
10
11
12
13
14
然后,如果我們?cè)赗unLoop中搜索一下 dispatch,可以搜索出來(lái) 130個(gè)結(jié)果。
接下來(lái),我們來(lái)看看RunLoop的主要實(shí)現(xiàn)邏輯中哪些地方用到的 GCD。

1.RunLoop 的超時(shí)時(shí)間
我們?cè)谇懊娼榻B過(guò)RunLoop 啟動(dòng)在 CoreFoudation 庫(kù)中有兩個(gè)API:

//mode默認(rèn)為defaultMode、超時(shí)時(shí)間是100億秒、false
void CFRunLoopRun(void)
// 可以設(shè)置mode、runloop 超時(shí)時(shí)間、是否處理完source立刻返回
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
1
2
3
4
而RunLoop 的超時(shí)時(shí)間就是使用 GCD 中的 dispatch_source_t來(lái)實(shí)現(xiàn)的,摘自 __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) { //超時(shí)時(shí)間在最大限制內(nèi),才創(chuàng)建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;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
如果看不懂這段源碼,可以先去看看GCD API 記錄 (三)中的 dispatch_source中的timer

2.執(zhí)行GCD MainQueue 上的異步任務(wù)
在__CFRunLoopRun方法的前幾行,有一個(gè)變量dispatchPort,它的作用是保存Main_Queue的port,便于后面RunLoop拿到GCD 主線程中的異步任務(wù)來(lái)執(zhí)行。

mach_port_name_t dispatchPort = MACH_PORT_NULL;

······
// 只有在MainRunLoop,才會(huì)有下面這行賦值,否則 dispatchPort 為NULL
dispatchPort = _dispatch_get_main_queue_port_4CF();
1
2
3
4
5
6
7
來(lái)看一下,RunLoop 是如何執(zhí)行GCD中MainQueue上的任務(wù)的:

// 中間去掉了一些宏判斷相關(guān)的邏輯代碼
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;
1
2
3
4
5
6
7
8
看來(lái),關(guān)鍵的邏輯都在 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上的異步任務(wù)并執(zhí)行
__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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
從上面的源碼片段可以看出,有判斷是否是在MainRunLoop,有獲取Main_Queue 的port,并且有調(diào)用 Main_Queue 上的回調(diào),這只能是是 GCD 主隊(duì)列上的異步任務(wù)。即:dispatch_async(dispatch_get_main_queue(), block)產(chǎn)生的任務(wù)。

RunLoop 與 Autorelease Pool的關(guān)系
RunLoop與 Autorelease Pool 有關(guān)系么?
有。
我們總是看到有文章說(shuō)程序啟動(dòng)后,蘋(píng)果在主線程 RunLoop 里注冊(cè)了兩個(gè) Observer:
第一個(gè) Observer 監(jiān)視的事件是 Entry(即將進(jìn)入Loop),其回調(diào)內(nèi)會(huì)調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動(dòng)釋放池。其 order 是-2147483647,優(yōu)先級(jí)最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。
第二個(gè) Observer 監(jiān)視了兩個(gè)事件: BeforeWaiting(準(zhǔn)備進(jìn)入睡眠) 和 Exit(即將退出Loop),
BeforeWaiting(準(zhǔn)備進(jìn)入睡眠)時(shí)調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池;
Exit(即將退出Loop) 時(shí)調(diào)用 _objc_autoreleasePoolPop() 來(lái)釋放自動(dòng)釋放池。這個(gè) Observer 的 order 是 2147483647,優(yōu)先級(jí)最低,保證其釋放池子發(fā)生在其他所有回調(diào)之后。

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

由 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
};
1
2
3
4
5
6
7
8
9
10
activities = 0x1,對(duì)應(yīng)的就是kCFRunLoopEntry;
activities = 0xa0,對(duì)應(yīng)的就是kCFRunLoopBeforeWaiting | kCFRunLoopExit 。

可能很多人看了上面的結(jié)論和Log 信息,都有這樣的疑惑:_wrapRunLoopWithAutoreleasePoolHandler()內(nèi)部是如何處理自動(dòng)釋放池的?你說(shuō)它釋放了舊的 AutoreleasePool,并新建了一個(gè)新的,就是這樣?
目前,我也不知道如何查看 _wrapRunLoopWithAutoreleasePoolHandler() 中的實(shí)現(xiàn),如果你有方式獲取到她的內(nèi)部信息,或者調(diào)用堆棧,歡迎告知我!

AutoreleasePool原理擴(kuò)展
這一小節(jié),全部摘自黑幕背后的Autorelease,你可以閱讀原文,了解更多 Autorelease 內(nèi)容。
ARC下,我們使用@autoreleasepool{}來(lái)使用一個(gè)AutoreleasePool,隨后編譯器將其改寫(xiě)成下面的樣子:

void *context = objc_autoreleasePoolPush();
// {}中的代碼
objc_autoreleasePoolPop(context);
1
2
3
而這兩個(gè)函數(shù)都是對(duì)AutoreleasePoolPage的簡(jiǎn)單封裝,所以自動(dòng)釋放機(jī)制的核心就在于這個(gè)類(lèi)。

AutoreleasePoolPage是一個(gè)C++實(shí)現(xiàn)的類(lèi)

AutoreleasePool并沒(méi)有單獨(dú)的結(jié)構(gòu),而是由若干個(gè)AutoreleasePoolPage以雙向鏈表的形式組合而成(分別對(duì)應(yīng)結(jié)構(gòu)中的parent指針和child指針)
AutoreleasePool是按線程一一對(duì)應(yīng)的(結(jié)構(gòu)中的thread指針指向當(dāng)前線程)
AutoreleasePoolPage每個(gè)對(duì)象會(huì)開(kāi)辟4096字節(jié)內(nèi)存(也就是虛擬內(nèi)存一頁(yè)的大?。?,除了上面的實(shí)例變量所占空間,剩下的空間全部用來(lái)儲(chǔ)存autorelease對(duì)象的地址
上面的id *next指針作為游標(biāo)指向棧頂最新add進(jìn)來(lái)的autorelease對(duì)象的下一個(gè)位置
一個(gè)AutoreleasePoolPage的空間被占滿(mǎn)時(shí),會(huì)新建一個(gè)AutoreleasePoolPage對(duì)象,連接鏈表,后來(lái)的autorelease對(duì)象在新的page加入
所以,若當(dāng)前線程中只有一個(gè)AutoreleasePoolPage對(duì)象,并記錄了很多autorelease對(duì)象地址時(shí)內(nèi)存如下圖:

圖中的情況,這一頁(yè)再加入一個(gè)autorelease對(duì)象就要滿(mǎn)了(也就是next指針馬上指向棧頂),這時(shí)就要執(zhí)行上面說(shuō)的操作,建立下一頁(yè)page對(duì)象,與這一頁(yè)鏈表連接完成后,新page的next指針被初始化在棧底(begin的位置),然后繼續(xù)向棧頂添加新對(duì)象。

所以,向一個(gè)對(duì)象發(fā)送- autorelease消息,就是將這個(gè)對(duì)象加入到當(dāng)前AutoreleasePoolPage的棧頂next指針指向的位置。

  • AutoreleasePool釋放*
    每當(dāng)進(jìn)行一次objc_autoreleasePoolPush調(diào)用時(shí),runtime向當(dāng)前的AutoreleasePoolPage中add進(jìn)一個(gè)哨兵對(duì)象,值為0(也就是個(gè)nil),那么這一個(gè)page就變成了下面的樣子:

objc_autoreleasePoolPush的返回值正是這個(gè)哨兵對(duì)象的地址,被objc_autoreleasePoolPop(哨兵對(duì)象)作為入?yún)?,于是?/p>

1.根據(jù)傳入的哨兵對(duì)象地址找到哨兵對(duì)象所處的page
2.在當(dāng)前page中,將晚于哨兵對(duì)象插入的所有autorelease對(duì)象都發(fā)送一次- release消息,并向回移動(dòng)next指針到正確位置
3.補(bǔ)充2:從最新加入的對(duì)象一直向前清理,可以向前跨越若干個(gè)page,直到哨兵所在的page
剛才的objc_autoreleasePoolPop執(zhí)行后,最終變成了下面的樣子:

Have Fun!

作者:Haley_Wong
來(lái)源:CSDN
原文:https://blog.csdn.net/u011619283/article/details/53783650
版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請(qǐng)附上博文鏈接!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容