跟多數(shù)開發(fā)者一樣,我也曾經(jīng)迷惑于runloop,最初只了解可以通過runloop一些監(jiān)聽事件的通知來做一些事情,優(yōu)化性能。關(guān)于runloop源碼的基礎(chǔ)知識(shí),本文不做論述,可以參考眾神的文章:
ibireme:《深入理解RunLoop》
sunyawang:《RunLoop系列之源碼分析》
xiaoxiaobukuang:《RunLoop》
本文主要內(nèi)容:
- 指出廣泛傳播runloop文章中錯(cuò)誤
- 通過代碼論證錯(cuò)誤
- 通過demo論證錯(cuò)誤
runloop解讀文章中的錯(cuò)誤
本人也看著眾神的文章才對(duì)runloop有了比較深入了解,最近自己終于利用零零星星的時(shí)間把runloop源碼也看了一遍,才發(fā)現(xiàn)好多人都誤解了runloop??!就拿下面這張好多文章中都提及的圖片和流程來說:

這是runloop運(yùn)行流程圖,但其實(shí)這個(gè)圖里面有兩個(gè)錯(cuò)誤,請(qǐng)看下面標(biāo)注圖:

第一個(gè)錯(cuò)誤 “source0(port)” 應(yīng)該是作者筆誤,圖中錯(cuò)誤將source1 (基于port)寫成source0;
第二個(gè)錯(cuò)誤 "5. 如果有source1,跳到第9步" 從圖和作者的代碼注釋中都能看出是理解有錯(cuò)誤,這里也正是本文重點(diǎn)描述的內(nèi)容
先說結(jié)論,再逐步驗(yàn)證:
這里其實(shí)判斷的是 主線程是否有需要處理的事件,如果沒有則跳到第9步,這里跟source1沒有關(guān)系!
所以應(yīng)該改成“5. 如果當(dāng)前是主線程的runloop,并且主線程有事兒,跳到第9步”
源碼論證
我們直接上源碼(版本CF-1151.16)分析一下,直接看這句話對(duì)應(yīng)的代碼(有精簡(jiǎ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;
}
}
可以看出跳轉(zhuǎn)到第9步(goto handle_msg)的邏輯是判斷__CFRunLoopServiceMachPort函數(shù)的返回值是否為真,而這個(gè)if對(duì)應(yīng)的就是上文描述“如果有source1”,那么這句話是這個(gè)意思嗎? 起初我也是這么認(rèn)為的,直到我看到了后面下一段第7步“休眠”的代碼:
// 第七步,進(jìn)入循環(huán)開始不斷的讀取端口信息,如果端口有喚醒信息則喚醒當(dāng)前runLoop
__CFPortSet waitSet = rlm->_portSet;
...
...
if (kCFUseCollectableAllocator)
{
memset(msg_buffer, 0, sizeof(msg_buffer));
}
// waitSet 為所有需要監(jiān)聽的port集合, TIMEOUT_INFINITY表示一直等待
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
這里面出現(xiàn)了上面的一樣的__CFRunLoopServiceMachPort方法, 單拎出來比對(duì)下,
__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy)
比較后發(fā)現(xiàn),參數(shù)中第一個(gè)參數(shù)和倒數(shù)第三個(gè)參數(shù)不同。我們通過__CFRunLoopServiceMachPort的源碼來分析下,其中重點(diǎn)關(guān)注:
- livePort的賦值用于函數(shù)外部使用;
- __CFRunLoopServiceMachPort方法中mach_msg的參數(shù)MACH_RCV_MSG表示在接收消息;
- __CFRunLoopServiceMachPort參數(shù)timeout對(duì)于二者入?yún)⒎謩e是0和TIMEOUT_INFINITY,分別表示查詢到立刻返回和一直等待有消息再返回;
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy)
{
Boolean originalBuffer = true;
kern_return_t ret = KERN_SUCCESS;
for (;;)
{ /* In that sleep of death what nightmares may come ... */
mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
msg->msgh_bits = 0;
msg->msgh_local_port = port;
msg->msgh_remote_port = MACH_PORT_NULL;
msg->msgh_size = buffer_size;
msg->msgh_id = 0;
if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);
// Take care of all voucher-related work right after mach_msg.
// If we don't release the previous voucher we're going to leak it.
voucher_mach_msg_revert(*voucherState);
// Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one.
*voucherState = voucher_mach_msg_adopt(msg);
if (voucherCopy)
{
if (*voucherState != VOUCHER_MACH_MSG_STATE_UNCHANGED)
{
*voucherCopy = voucher_copy();
}
else
{
*voucherCopy = NULL;
}
}
CFRUNLOOP_WAKEUP(ret);
if (MACH_MSG_SUCCESS == ret)
{
*livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
return true;
}
if (MACH_RCV_TIMED_OUT == ret)
{
if (!originalBuffer) free(msg);
*buffer = NULL;
*livePort = MACH_PORT_NULL;
return false;
}
if (MACH_RCV_TOO_LARGE != ret) break;
buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
if (originalBuffer) *buffer = NULL;
originalBuffer = false;
*buffer = realloc(*buffer, buffer_size);
}
HALT;
return false;
}
從代碼中我們可以大概看出,休眠時(shí)調(diào)用這個(gè)方法的作用就是監(jiān)聽判斷waitSet中所有port,如果這些port中有一個(gè)出現(xiàn)消息,就喚醒了跳出休眠,并且將喚醒的port賦值給livePort。對(duì)于上面的mach_msg,我們?cè)诔绦蜻\(yùn)行時(shí)打斷點(diǎn)一定經(jīng)常遇到,如下圖,當(dāng)runloop處于休眠時(shí),就是下面的狀態(tài),也就是上面代碼中mach_msg的timeout入?yún)門IMEOUT_INFINITY時(shí)阻塞式等待的情況:

下面的代碼也驗(yàn)證了livePort用來判斷是哪種激勵(lì)將休眠喚醒,通過livePort來判斷是進(jìn)行哪種處理:
if (MACH_PORT_NULL == livePort)
{
CFRUNLOOP_WAKEUP_FOR_NOTHING();
}
else if (livePort == rl->_wakeUpPort)
{
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
}
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort)
{
// 處理timer
}
else if (livePort == dispatchPort)
{
......
// 處理主線程隊(duì)列中事件
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
......
}
else
{
......
// 處理Source1
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
......
}
通過上面對(duì)__CFRunLoopServiceMachPort的源碼分析:我們基本確定了,第5步對(duì)應(yīng)的代碼
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL))
{
goto handle_msg;
}
其實(shí)__CFRunLoopServiceMachPort在等的是dispatchPort這個(gè)端口的消息,而這個(gè)端口是什么呢? 我們順著源碼向前找:
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)));
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name))
dispatchPort = _dispatch_get_main_queue_port_4CF();
我們重點(diǎn)看if判斷中的 (CFRunLoopGetMain() == rl),其中rl表示當(dāng)前的runloop,查看CFRunLoopGetMain()源碼可知返回的是主線程的runloop,所以這里判斷就是當(dāng)前runloop是否是主線程的runloop,這時(shí)我們?cè)倩氐较旅嫣D(zhuǎn)到handle_msg那段代碼:
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;
}
}
我們可以看到判斷是否跳轉(zhuǎn)之前先判斷dispatchPort有沒有消息,而再之前的條件必須滿足MACH_PORT_NULL != dispatchPort,也就是前面必須對(duì)dispatchPort有所賦值,才會(huì)進(jìn)行下面的判斷和跳轉(zhuǎn)邏輯。所以這里可以小總結(jié)一下重要的結(jié)論:
- 只有當(dāng)前運(yùn)行的runloop是主線程的runloop時(shí),才會(huì)對(duì)dispatchPort賦值;
- 如果dispatchPort沒有賦值,則不會(huì)進(jìn)行是否“goto handle_msg”的邏輯判斷;
- dispatchPort賦予的值是主線程隊(duì)列對(duì)應(yīng)的port;
- 如果當(dāng)前運(yùn)行的runloop不是主線程的runloop,那么原圖中的第5步就不會(huì)存在,也就是多子線程圖中不存在第5步;
綜上,終于來到我們理論的總結(jié):原圖中第5步的應(yīng)該由"5. 如果有source1,跳到第9步"改成“5. 如果當(dāng)前是主線程的runloop,并且主線程有事兒,跳到第9步”。 所以最終整體流程應(yīng)該是:
1. 通知observer run loop被觸發(fā)
2. 如果有timers事件的話,通知observer
3. 如果有source0要處理的話,通知observer
4. 觸發(fā)所有的準(zhǔn)備完畢的source0
5. 如果當(dāng)前是主線程的runloop,并且主線程有事兒,跳到第9步
6. 通知Observer runloop將進(jìn)入sleep狀態(tài)
7. mach進(jìn)入sleep和監(jiān)聽狀態(tài)
8. 通知observer,runloop被woke up
9. 如果runloop是被喚醒,CFRUNLOOP_WAKEUP_FOR_WAKEUP
10. 如果用戶定義的timer被觸發(fā),處理event并重啟RunLoop
11. 如果dispatchPort,處理主線程
12. 如果一個(gè)source1被觸發(fā),__CFRunLoopDoSource1
13. 繼續(xù)循環(huán)或通知observer runloop將要exited。
demo論證
最后我們?cè)儆胐emo來佐證一下,demo中我會(huì)首先則監(jiān)聽主線程的runloop,然后再在子線程監(jiān)聽子線程的runloop,打印監(jiān)聽的事件。
先看下demo中的主要代碼:
// 添加主線程runloop監(jiān)聽者
[self addMainObserver];
// 添加子線程runloop監(jiān)聽者
[self addOtherObserver];
// 此處使用sleep是為了避免使用timer造成runloop的timer事件的干擾。
sleep(3);
dispatch_async(dispatch_get_main_queue(), ^{
CGFloat randomAlpha = (arc4random() % 100)*0.01;
[self.view setBackgroundColor:[UIColor colorWithWhite:0.5 alpha:randomAlpha]];
});
...
...
// 添加子線程runloop監(jiān)聽者
- (void)addOtherObserver
{
[NSThread detachNewThreadWithBlock:^{
_timer = [NSTimer scheduledTimerWithTimeInterval:3 repeats:NO block:^(NSTimer * _Nonnull timer)
{
NSLog(@"###cmm子線程###timer時(shí)間到");
}];
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"###cmm子線程###進(jìn)入kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"###cmm子線程###即將處理Timer事件");
break;
case kCFRunLoopBeforeSources:
NSLog(@"###cmm子線程###即將處理Source事件");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"###cmm子線程###即將休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"###cmm子線程###被喚醒");
break;
case kCFRunLoopExit:
NSLog(@"###cmm子線程###退出RunLoop");
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
CFRunLoopRun();
}];
}
// 添加主線程runloop監(jiān)聽者
- (void)addMainObserver
{
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"###cmm###進(jìn)入kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"###cmm###即將處理Timer事件");
break;
case kCFRunLoopBeforeSources:
NSLog(@"###cmm###即將處理Source事件");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"###cmm###即將休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"###cmm###被喚醒");
break;
case kCFRunLoopExit:
NSLog(@"###cmm###退出RunLoop");
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
_timer1 = [NSTimer scheduledTimerWithTimeInterval:3 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"###cmm###timer時(shí)間到");
}];
}
結(jié)合剛才整理的runloop的整體流程分析一下預(yù)期的打印結(jié)果應(yīng)該是:
- 主線程中,如果有事兒需要處理, “即將處理timer事件”-->"即將處理source事件"-->下一個(gè)循環(huán)的"即將處理timer事件"-->"即將處理source事件",這里沒有經(jīng)過“即將休眠”,就是因?yàn)橹骶€程有事兒,進(jìn)入“goto handle_msg”,直接跳過休眠階段。
- 子線程在主線程runloop處理事兒的時(shí)候,并沒有打印結(jié)果變化,說明并沒有觸發(fā)這個(gè)goto條件。
demo跑起來~~~
我們?cè)谥骶€程的代碼中打斷點(diǎn),查看堆棧和日志如下圖:

可以發(fā)現(xiàn),如我們所料:主線程的runloop在即將處理source事件后,直接跳到了 “__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__” ,也就是跳過了休眠,直接到了handle_msg對(duì)應(yīng)的 else if (livePort == dispatchPort) 分支。另外我們可以在日志中發(fā)現(xiàn)此時(shí)子線程的runloop已經(jīng)啟動(dòng),并處于休眠狀態(tài)。
然后我們注意下下圖:

如圖中箭頭處,在我們程序跳過斷點(diǎn)繼續(xù)執(zhí)行后,并沒有子線程的相關(guān)打印,說明此時(shí)子線程的runloop并不會(huì)管主線程那部分代碼。