RunLoop 是什麼
強烈推薦
ibireme
大神的文章深入理解RunLoop
關於 Runloop
,儘管早就知道它的本質實現是一個迴圈,但筆者還是一直很困惑它的作用是什麼 ,不過最近整理相關知識總算是理解了。
程式碼的執行邏輯是自上而下的,如果沒有 Runloop
,程式碼執行完畢後,程式就退出了,對應到實際場景就是 APP
一開啟立馬就退出了。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"程式執行中...");
}
return 0;
}
// log
程式執行中...
Program ended with exit code: 0
複製程式碼
例如上面的程式碼,程式碼執行完畢後,main
函式返回,然後程式退出。
為什麼工作中,好像沒有編寫 Runloop
相關的程式碼,程式還是能夠穩定持續執行呢?
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
複製程式碼
這是因為程式自動幫我們在 UIApplicationMain…
中做了這個事情。
下面來看看 Runloop
的簡化的虛擬碼,主要來自 sunnyxx 大神的一次視訊分享:
function loop() {
do {
有事幹了 = 我睡覺了沒事別找我();
if (搬磚) {
搬磚();
} else if (吃飯) {
吃飯();
}
} while (活著)
}
複製程式碼
這個虛擬碼看著還是有一點抽象,需要了解的一個知識點是執行緒和 RunLoop
之間是一一對應的,這裡的睡覺了可以理解為執行緒休眠 [NSThread sleepUntilDate:...]]
,也就是說當應用沒有任何事件觸發時,就會停在睡覺那行程式碼不執行,這樣就節約了 CPU
的運算資源,提高程式效能,直到有事件喚醒應用為止。例如上面的搬磚事件,吃飯事件。處理完後,又會進入睡覺狀態直到下次喚醒,反覆迴圈,這樣就保證了程式能隨時處理各種事件並能夠穩定執行。
實際上觸控事件、螢幕 UI
重新整理、延遲迴調等等都是 Runloop
實現的。
Runloop 的結構
先來看看 Runloop
的結構原始碼:
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
// ...
};
複製程式碼
這裡包含一個執行緒的成員變數 _pthread
,可以看出 Runloop
確實和執行緒是息息相關的。還能看到 Runloop
擁有很多關於 Model
的成員變數,再來看看 Model
的結構:
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
// ...
};
複製程式碼
先不管這些東西是幹什麼的,至少我們現在能夠得出如下圖所示的理解:
一個 Runloop
中包含若干個 Model
,每個 Mode
又包含若干個 Source/Timer/Observer
。
Runloop 的 Model
Model
代表 Runloop
的執行模式,Runloop
每次只能指定一個 Model
作為 _currentMode
,如果需要切換 Mode
,只能退出當前 Loop
,再重新選擇一個 Mode
進入。主執行緒的 Runloop
這裡有兩個預置的模式 ,並且這也是系統公開的兩個 Model
:
-
kCFRunLoopDefaultMode:
APP
的普通狀態,通常主執行緒是在這個Mode下執行,已被標記為Common
。 -
UITrackingRunLoopMode:
App
追蹤觸控ScrollView
滑動時的狀態,保證介面滑動時不受其他Mode
影響,已被標記為Common
。
注意 Runloop
的結構中有一個 _commonModes
。這裡是因為一個 Mode
可以將自己標記為 Common
(通過將其 ModeName
新增到 RunLoop
的 commonModes
中 ),標記為 Common
的 Model
都可以處理事件,可以理解為變相的實現了多個 Model
同時執行。同時系統也提供了一個操作 Common
標記的字串->kCFRunLoopCommonModes
。如果我們想要上面兩種模式下都能處理事件,就可以使用這個字串。
Model 中的 Item
Source/Timer/Observer
被統稱為 mode item,不同 Model
的 Source0/Source1/Timer/Observer
被分隔開來,互不影響,如果 Mode
裡沒有任何Source0/Source1/Timer/Observer
,RunLoop
會立馬退出。
Source
Source
是事件產生的的地方,它對應的類為 CFRunLoopSourceRef
。Source
有兩個版本:Source0
和 Source1
。
Source0
只包含了一個回撥(函式指標),它並不能主動觸發事件。Source1
包含了一個mach_port
和一個回撥(函式指標),被用於通過核心和其他執行緒相互傳送訊息。這種Source
能主動喚醒RunLoop
的執行緒。例如螢幕觸控、鎖屏和搖晃等。
Timer
Timer
對應的類是 CFRunLoopTimerRef
,它其實就是 NSTimer
,當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回撥。
Observer
Observer
對應的類是 CFRunLoopObserverRef
,當 RunLoop 的狀態發生變化時,觀察者就能通過回撥接受到這個變化。可以觀測的時間點有以下幾個:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
複製程式碼
Runloop 的內部邏輯
開啟開頭的 Runloop
的原始碼,面對眾多程式碼,讓人毫無頭緒,但是前文中已經講到,螢幕的觸控事件是 Runloop
來處理的。於是打個斷點,來檢視程式的函式呼叫棧:
從圖中能看到,Runloop
是從 11
開始的,於是從原始碼中搜尋 CFRunLoopRunSpecific
函式,這裡只探究內部主要邏輯,其他細節不看,下面是精簡後的函式:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
// 根據 modeName 獲取currentMode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
// 設定 Runloop 的 Model
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
// 通知 Observers: 即將進入 RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 進入 runloop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知 Observers: RunLoop 即將退出
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
複製程式碼
然後再進入 __CFRunLoopRun(...)
函式檢視內部精簡後的主要邏輯原始碼:
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);
// 處理 Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 處理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
// 判斷有無 Sources1
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
// 跳轉到 handle_msg 處理 Sources1soso
goto handle_msg;
}
// 通知 Observers: 即將休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 開始休眠
__CFRunLoopSetSleeping(rl);
// 等待訊息喚醒當前執行緒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
// 結束休眠
__CFRunLoopUnsetSleeping(rl);
// 通知 Observers: 結束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 處理
handle_msg:;
// 被 timer 喚醒
if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
// 處理 timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
}
// 被 gcd 喚醒
else if (livePort == dispatchPort) {
// 處理 gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
// 被source1喚醒
} else {
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
}
// 處理 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
內部確實是一個迴圈,並且,喚醒 RunLoop
的方式有 mach port
、Timer
和 dispatch
。筆者最初在疑惑一個問題,上面的函式呼叫棧是一個點選螢幕後的響應事件,可以看出這裡是 sources0
,明明是一個觸控事件為什麼不是 sources1
呢,筆者猜測 sources1
這裡喚醒了 Runloop
,因為 sources0
是無法喚醒 runloop
的,然後再在 sources0
的回撥中處理的點選事件。
RunLoop 中的 mach port
這裡由於目前筆者水平有限,只能夠理解到 mach port
是一個可以控制硬體和接受硬體反饋的一個系統,然後可以通過它將來自硬體的操作轉化成熟知的 UIEvent
事件等等。
總結
這篇文章主要講解了 Runloop
到底是一個什麼東西,當然 Runloop
的知識不僅僅只有這篇文章這點。例如實際用處中的執行緒保活(AFNetworking 2.x 版本中),滑動時 Timer
怎麼不被停止,自動釋放池的實現等等都用到了 Runloop
。