RunLoop 四: RunLoop原始碼學習
阿新 • • 發佈:2018-11-07
一、前言
- 因為RunLoop的原始碼涉及到C語言,所以比較抽象。
- 在蘋果網站上下載 RunLoop 的原始碼,地址為:https://opensource.apple.com/tarballs/CF/
- 找到 CFRunLoop.c 檔案,進行學習。
- 需要找到RunLoop 的入口檔案,也就是上個筆記上寫的 ‘通知Observers:進入Loop’ 這句話。
二、如何找到 RunLoop 的入口
- 新建一個iOS專案,在ViewController.m 檔案中 寫一個
touchesBegan:withEvent
方法,在這個方法中打上斷點. - 為何在 這個方法中打上斷點,因為 點選事件就是由 RunLoop來處理的。在這個方法中打上斷點,就可以看到到底是RunLoop的哪一個函式來進行呼叫的。也就是要在 這個方法中看下函式呼叫棧。
- 函式呼叫棧
- 可以看到函式呼叫棧是從下往上開始查詢的。最開始的入口是
UIApplicationMain
, 依次是:GSEventRunModal
、CFRunLoopRunSpecific
、__CFRunLoopRun
、__CFRunLoopDoSources0
、__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
、直到 呼叫touchesBegan:withEvent:
方法 - 我們可以在runloop.c檔案中查詢
CFRunLoopRunSpecific
函式。也就是 用CFRunLoopRunSpecific
- 可以在
CFRunLoop.c
檔案中搜索到函式:SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
三、理順 CFRunLoopRunSpecific 函式的流程
注: 只保留 CFRunLoopRunSpecific 中主要的程式碼。
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ // 通知 Observers: 進入 Loop // currentMode : 進入到某種模式 __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); // 具體要做的事情 // __CFRunLoopRun 這個可以在函式棧中看到。 result == __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); // 通知 Observers: 退出 Loop __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); return result; }
四、__CFRunLoopRun 函式做了哪些事情
- 在這個函式中你會看到一個
do { ... ... } while(0 == retVal)
的迴圈。這個迴圈的含義是,如果 do 後面的大括號 裡面的程式碼返回值等於 0 (也就是 while 條件成立的話),會一直執行 do 裡面的程式碼 - 如果 條件一直成立,它就會一直迴圈的做do裡面的事情。
- do while 迴圈 裡面的內容,就是 runloop 具體要做的事情
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 通知 Observers: 即將處理 Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 Observers: 即將處理Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 處理 blocks
__CFRunLoopDoBlocks(rl, rlm);
// 處理 source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
// 如果返回 Yes ,再次處理blcoks
if (sourceHandledThisLoop) {
// 處理 blocks
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 判斷 有沒有 Sources1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
// 如果有 Sources1, 就跳轉到 handle_msg
goto handle_msg;
}
didDispatchPortLastTime = false;
// 通知 Observers: 即將休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
do {
// 等待別的訊息來喚醒當前執行緒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// 如果沒有訊息來喚醒,就會阻塞在這裡。一直等到訊息來喚醒才會往下執行程式碼。
} while (1);
__CFRunLoopUnsetSleeping(rl);
// 通知 Observers:結束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
if (被timer喚醒) {
// 處理 Timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
} else if (被 GCD 喚醒) {
// 處理 GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else {// 被 Source1 喚醒
// 處理 Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
}
// 處理 blocks
__CFRunLoopDoBlocks(rl, rlm);
// 設定返回值
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
五、 RunLoop 每個階段裡面所呼叫的函式
每一個步驟裡面都會呼叫一個很長的方法,這個方法是處理 UIKit 的。
01、通知Observers:進入Loop:
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
04、處理Blocks:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
08、通知Observers:結束休眠(被某個訊息喚醒)
- 01> 處理Timer:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
- 02> 處理GCD Async To Main Queue :
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
- 03> 處理Source1 :
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
09、處理Blocks
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
例如:
(一)、 NSTimer
- 當到了
_CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
,在上面的 #1 則是 Foundation 框架的 NSFireTimer 了。
(二)、特殊的GCD
- 一般來說 GCD 有自己的邏輯來處理。
- GCD 有很多東西是不依賴 RunLoop 處理的。
- 也就是說: GCD 是 GCD ; RunLoop 是 RunLoop 。 他們是分開的。
- 但GCD 有一種情況,會交給 RunLoop 來處理。如下圖:
- 列印它的函式棧
六、休眠的細節
(一)、複習
- 在
07:通知Observers:開始休眠(等待訊息喚醒)
這條如果沒有訊息,就開始睡覺。 - 開始睡覺,可以當成是執行緒阻塞,不會繼續往下走。
- 程式碼為:
do {
// 等待別的訊息來喚醒當前執行緒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// 如果沒有訊息來喚醒,就會阻塞在這裡。一直等到訊息來喚醒才會往下執行程式碼。
} while (1);
- 休眠意味著當前執行緒不做事情,CPU 不會給 當前執行緒任何資源,當前執行緒就沒有事情可做,就睡覺。程式碼就會停留在這個位置,不會再往下走。
- 一旦使用者喚醒它,才會繼續往下走。執行緒開始做事情。
(二)、執行緒阻塞是如何實現的
-
如果 寫成
while(1){... ... }
形式的執行緒阻塞,是通過程式碼進行阻塞,當前執行緒並沒有休息。因為這句程式碼是死迴圈,一直在判斷條件,條件成句,執行語句;條件成句,執行語句;一直在執行程式碼,相當於當前執行緒一直在執行程式碼,沒有休息。這樣 cpu 並沒有休息。 -
但
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy)
這句程式碼,會直接讓 當前執行緒睡覺。 -
怎麼會達到這種效果?
- 在 RunLoop.c -> __CFRunLoopServiceMachPort 方法中,有
mach_msg(msg,MACH_RCV_MSG|...)
這行程式碼 - 當看到 mach_msg 代表 核心 函式。它就能直接睡覺。
- 要想達到直接睡覺,只有核心層面才能辦到。
- 在 RunLoop.c -> __CFRunLoopServiceMachPort 方法中,有
-
API 分為:
- 核心層面API : 非常底層的API,作業系統底層的API。
- 例如: 讓系統直接死掉的API、讓系統直接休眠的API或者直接操作硬體層面的一些程式碼。
- 一般不開放給 程式設計師 使用。因為比較危險。
- 應用層面API :
- 開放給 程式設計師使用。可以搭建一些介面、傳送網路請求之類的。
- 核心層面API : 非常底層的API,作業系統底層的API。
-
RunLoop休眠的實現原理
- 有兩大類,使用者方面和核心方面。當用戶呼叫
mach_msg()
方法時,mach_msg()
會直接呼叫 核心裡面的mach_msg()
. - 核心的
mach_msg()
方法等待訊息。如果沒有沒有訊息就讓執行緒休眠,有訊息就喚醒執行緒。 - 當有訊息的時候,核心的
mach_msg()
會告訴 使用者 , 處理訊息。
七、面試題:
- runloop內部實現邏輯?
- 就是上邊的執行邏輯圖
- runloop和執行緒的關係?
- 一對一的關係
- runloop 是怎麼響應使用者操作的, 具體流程是什麼樣的?
- 首先是由 Source1 捕捉系統事件。
- 相當於一旦使用者點選螢幕,先是由 Source1 去處理這個事件,然後 Source1 把這個事件包裝事件佇列 EventQueen,.
- 事件佇列 EventQueen 是由 source0 處理。
- 說說runLoop的幾種狀態說說runLoop的幾種狀態
- kCFRunLoopEntry: 進入 RunLoop 迴圈
- kCFRunLoopBeforeTimers: 處理 定時器(Timers
- kCFRunLoopBeforeSources: 處理 Sources
- kCFRunLoopBeforeWaiting: 休眠之前
- kCFRunLoopAfterWaiting: 被喚醒
- kCFRunLoopExit: 退出當前 RunLoop
- runloop的mode作用是什麼?
- kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的預設Mode,通常主執行緒是在這個Mode下執行
- UITrackingRunLoopMode:介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響
作用:
- 隔離。
- 將不同的Source0/Source1/Timer/Observer隔離開來。
這樣每一組都不會互相影響。