RunLoop
RunLoop概念
RunLoop理解為運行循環(huán)。其本質就是一個do-while,這里的do-while和普通的do-while循環(huán)不一樣,一般的 while 循環(huán)會導致 CPU 進入忙等待狀態(tài),而 Runloop 則是一種“閑”等待,當沒有事件時,Runloop 會進入休眠狀態(tài),有事件發(fā)生時, Runloop 會去找對應的 Handler 處理事件。Runloop 可以讓線程在需要做事的時候忙起來,不需要的話就讓線程休眠。

圖中展現(xiàn)了 Runloop 在線程中的作用:從 input source 和 timer source 接受事件,然后在線程中處理事件。
RunLoop 作用
- 保持程序持續(xù)運行。程序啟動就會自動開啟一個runloop在主線程
- 處理App中的各種事件
- 節(jié)省CPU資源,提高程序性能 有事情則做事,沒事情則休眠
RunLoop 和線程之間的關系
- Runloop 和線程是綁定在一起的。每個線程(包括主線程)都有一個對應的 Runloop 對象。我們并不能自己創(chuàng)建 Runloop 對象,但是可以獲取到系統(tǒng)提供的 Runloop 對象。
- 主線程的 Runloop 會在應用啟動的時候完成啟動,其他線程的 Runloop 默認并不會啟動,需要我們手動啟動。
底層RunLoop的存儲使用的是字典結構,線程是key,對應的runloop是value。
- 主線程是默認開啟RunLoop的
- 子線程是默認不開啟RunLoop的
- 子線程開啟RunLoop需要我們手動開啟,手動開啟時會先在全局的存儲字典里根據(jù)傳入的線程key,查看value是否存在,不存在的話就基于線程為key創(chuàng)建一個新的runloop并存儲進字典里。
RunLoop Mode
RunLoop Mode可以理解為RunLoop的運行模式,在蘋果文檔里定義了有五種運行模式。
- kCFRunLoopDefaultMode, App的默認運行模式,通常主線程是在這個運行模式下運行
- UITrackingRunLoopMode, 跟蹤用戶交互事件(用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他Mode影響)
- kCFRunLoopCommonModes, 偽模式,不是一種真正的運行模式
- UIInitializationRunLoopMode:在剛啟動App時第進入的第一個Mode,啟動完成后就不再使用
- GSEventReceiveRunLoopMode:接受系統(tǒng)內(nèi)部事件,通常用不到
在源碼里搜索__CFRunLoopMode可以看到里面的內(nèi)部結構,提取出關鍵的成員變量。
- _name:Mode的名字
- _sources0:
- App內(nèi)部事件,由App自己管理的,像UIEvent、CFSocket、以及performSelector不帶afterDelay參數(shù)的方法都是source0
- source0不能主動觸發(fā),需要調(diào)用CFRunLoopWakeUp(runloop) 來喚醒 RunLoop
- _sources1:
- 由RunLoop和內(nèi)核管理,Mach port驅動,如CFMachPort、CFMessagePort。
- source1包含了一個 mach_port 和一個回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息
- 能主動喚醒 RunLoop 的線程
- _observers:添加的觀察者
- _timers:定時器,包括NSTimer、CADisplayLink以及performSelector帶afterDelay參數(shù)的方法。
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
__CFPortSet _portSet;
}
RunLoop的本質
RunLoop底層其實就是一個結構體,通過源碼搜索__CFRunLoop。
提取出關鍵的成員變量_pthread、_currentMode以及_modes,可以發(fā)現(xiàn)
- RunLoop和線程是綁定的,每個線程會有對應的RunLop,只是主線程是默認開啟的,子線程不是默認開啟的,需要手動開啟,然后底層就會根據(jù)傳入的線程創(chuàng)建新的RunLoop
- _currentMode:當前運行的Mode
- _modes:多個mode的集合
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
通過上面的整理可以發(fā)現(xiàn)
- RunLoop管理著多個mode
- 每次RunLoop運行的的時候都必須指定一個mode作為_currentMode,如果需要執(zhí)行其他mode的事務,那么就要先退出當前mode,然后切換另一個mode
- mode里面管理著source0、source1、timers、observers以及port端口的事務。
引用來自掘金博主的圖片??

RunLoop的核心方法
通過查找源碼,我們可以發(fā)現(xiàn)三個關鍵的方法
//通知觀察者即將進入RunLoop
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
//進入RunLoop的關鍵方法
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//通知觀察者即將退出RunLoop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopRun
通過下面的源代碼解析,可以將RunLoop的處理邏輯理為
- 通知觀察者,即將進入runloop
- 通知觀察者,即將處理Timers
- 通知觀察者,即將處理Sources
- 執(zhí)行加入到runloop的blocks
- 處理Source0,處理之后可能會再次處理blocks
- 如果存在Source1,直接跳轉到第9步
- 通知觀察者,即將進入休眠
- 通知觀察者,結束休眠
- 執(zhí)行喚醒RunLoop的事件
- 處理timer事件
- 執(zhí)行gcd通過異步函數(shù)提交任務到主線程的Block
- 處理Source1事件
- 被手動喚醒,不需要執(zhí)行事件,單純起到喚醒RunLoop的功能
- 執(zhí)行加入到runloop的blocks
- 根據(jù)以下情況來確定是否要退出當前RunLoop
- 進入run方法時參數(shù)表明處理完事件就返回。
- 超出run方法參數(shù)中的超時時間
- 被外部調(diào)用者強制停止了
- 調(diào)用_CFRunLoopStopMode將mode停止了
- 檢測還有沒有待處理的sources/timer/observer
- 以上??五種情況均無的話 那么就跳轉到第2步,繼續(xù)循環(huán)
- 通知觀察者,退出RunLoop
以下代碼引用掘金博主的源碼解析
do {
// 消息緩沖區(qū),用戶緩存內(nèi)核發(fā)的消息
uint8_t msg_buffer[3 * 1024];
//取所有需要監(jiān)聽的port
__CFPortSet waitSet = rlm->_portSet;
//設置RunLoop為可以被喚醒狀態(tài)
__CFRunLoopUnsetIgnoreWakeUps(rl);
//1.通知 Observers: RunLoop 即將處理 Timer 回調(diào)。
if (rlm->_observerMask & kCFRunLoopBeforeTimers)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//2。通知 Observers: RunLoop 即將觸發(fā) Source(非port) 回調(diào)
if (rlm->_observerMask & kCFRunLoopBeforeSources)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 執(zhí)行被加入的block
//外部通過調(diào)用CFRunLoopPerformBlock函數(shù)向當前runloop增加block。新增加的block保存咋runloop.blocks_head鏈表里。
//__CFRunLoopDoBlocks會遍歷鏈表取出每一個block,如果block被指定執(zhí)行的mode和當前的mode一致,則調(diào)用__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__執(zhí)行
__CFRunLoopDoBlocks(rl, rlm);
//RunLoop 觸發(fā) Source0 (非port) 回調(diào)
// __CFRunLoopDoSources0函數(shù)內(nèi)部會調(diào)用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函數(shù)
//__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函數(shù)會調(diào)用source0的perform回調(diào)函數(shù),即rls->context.version0.perform
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
//處理了source0后再次處理blocks
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}
//
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
didDispatchPortLastTime = false;
//如果有 Source1 (基于port) 處于 ready 狀態(tài),直接處理這個 Source1 然后跳轉去處理消息。
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
//通知oberver即將進入休眠狀態(tài)
if(!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
//接收waitSet端口的消息
//等待接受 mach_port 的消息。線程將進入休眠
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// 計算線程沉睡的時長
rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopSetIgnoreWakeUps(rl);
// runloop置為喚醒狀態(tài)
__CFRunLoopUnsetSleeping(rl);
// 8. 通知 Observers: RunLoop對應的線程剛被喚醒。
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
//收到處理的消息進行處理
handle_msg:;
// 忽略端口喚醒runloop,避免在處理source1時通過其他線程或進程喚醒runloop(保證線程安全)
__CFRunLoopSetIgnoreWakeUps(rl);
if(MACH_PORT_NULL == livePort){
// livePort為null則什么也不做
}else if(livePort == rl->_wakeUpPort){
// livePort為wakeUpPort則只需要簡單的喚醒runloop(rl->_wakeUpPort是專門用來喚醒runloop的)
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
}else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort){
//如果是一個timerPort
// 如果一個 Timer 到時間了,觸發(fā)這個Timer的回調(diào)
// __CFRunLoopDoTimers返回值代表是否處理了這個timer
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
__CFArmNextTimerInMode(rlm, rl);
}
}else if(livePort == dispatchPort){
//如果是GCD port
//處理GCD通過port提交到主線程的事件
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}else{
// 處理source1事件(觸發(fā)source1的回調(diào))
//// runloop 觸發(fā)source1的回調(diào),__CFRunLoopDoSource1內(nèi)部會調(diào)用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}
/// 執(zhí)行加入到Loop的block
__CFRunLoopDoBlocks(rl, rlm);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 進入loop時參數(shù)說處理完事件就返回。
retVal = kCFRunLoopRunHandledSource; // 4
} else if (timeout_context->termTSR < mach_absolute_time()) {
/// 超出傳入?yún)?shù)標記的超時時間了
retVal = kCFRunLoopRunTimedOut; // 3
} else if (__CFRunLoopIsStopped(rl)) {
/// 被外部調(diào)用者強制停止了
__CFRunLoopUnsetStopped(rl); // 2
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
// 調(diào)用了_CFRunLoopStopMode將mode停止了
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped; // 2
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// source0/source1/timers/blocks一個都沒有了
retVal = kCFRunLoopRunFinished; // 1
}
}while(0 == retVal)
__CFRunLoopDoBlocks
解析關于runloop調(diào)用的blocks,而Source0、Source1、timers以及Observers 在前面已經(jīng)解析過了。
這個方法主要處理Blocks回調(diào)。
- runloop對象有一個_block_item結構的鏈表,里面存儲著當前runloop待處理的blocks
- 執(zhí)行
__CFRunLoopDoBlocks方法就是遍歷runloop的鏈表,取出blocks,然后執(zhí)行__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);方法 - KVO回調(diào)方法,以及在主線程調(diào)用的block;這兩種情況回調(diào)由
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);方法調(diào)用執(zhí)行
CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE(msg);
- 在調(diào)用GCD的異步函數(shù)
dispatch_async,往主線程里添加任務時會觸發(fā)該方法 - 由于子線程默認是沒有開啟Runloop的,子線程下執(zhí)行GCD的任務是不會被添加到RunLoop上的,子線程blocks的執(zhí)行是由
lidbdispatch驅動完成的。
RunLoop的應用
AutoreleasePool
App啟動后,蘋果在主線RunLoop里注冊了兩個觀察者,分別觀察RunLoop的即將進入loop事件、即將進入休眠事件、即將退出事件。
- 即將進入loop事件觸發(fā)時會調(diào)用
_objc_autoreleasePoolPush()方法創(chuàng)建自動釋放池以及插入哨兵對象 - 即將進入休眠事件觸發(fā)時會分別調(diào)用
_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()方法,釋放舊的自動釋放池并release里面的對象以及創(chuàng)建新的自動釋放池 - 即將退出事件觸發(fā)時會調(diào)用
_objc_autoreleasePoolPop()方法,釋放自動釋放池。
NSTimer
NSTimer就是上文提到的timer
- 底層
Runloop會在每個時間點都注冊一個事件,到了事件點進行回調(diào),但是并不是每次都是很準確地在指定時間點進行回調(diào)的,timer中有一個屬性Tolerance標識可以容忍多大的誤差。 - 如果
RunLoop有很多要處理的事,錯過了timer指定的時間點,那么是會錯過此次回調(diào)的。 - 默認在主線創(chuàng)建的
timer都會自動加入到RunLoop中,而如果是在子線程中創(chuàng)建的timer,在沒有開啟RunLoop的情況下,其實是無效的。
CADisplayLink
-
CADisplayLink是一個執(zhí)行頻率(fps)和屏幕刷新相同的定時器,需要加入到RunLoop才能執(zhí)行。但是我們也可以通過調(diào)用API來實現(xiàn)更改執(zhí)行頻率。 - 它與
NSTimer都是定時器,區(qū)別在于CADisplayLink的精度更高,而NSTimer的使用范圍更加地廣泛。
事件響應
- 蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統(tǒng)事件,其回調(diào)函數(shù)為
__IOHIDEventSystemClientQueueCallback()。 - 當一個硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉發(fā)給需要的App進程。隨后蘋果注冊的那個 Source1 就會觸發(fā)回調(diào),并調(diào)用
_UIApplicationHandleEventQueue()進行應用內(nèi)部的分發(fā)。 -
_UIApplicationHandleEventQueue()會把IOHIDEvent處理并包裝成UIEvent進行處理或分發(fā),其中包括識別 UIGesture/處理屏幕旋轉/發(fā)送給 UIWindow 等。通常事件比如 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調(diào)中完成的。
手勢識別
- 當上面的
_UIApplicationHandleEventQueue()識別了一個手勢時,其首先會調(diào)用Cancel將當前的 touchesBegin/Move/End 系列回調(diào)打斷。隨后系統(tǒng)將對應的 UIGestureRecognizer 標記為待處理。 - 蘋果注冊了一個
Observer監(jiān)測BeforeWaiting(Loop即將進入休眠) 事件,這個Observer的回調(diào)函數(shù)是_UIGestureRecognizerUpdateObserver(),其內(nèi)部會獲取所有剛被標記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)。 - 當有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時,這個回調(diào)都會進行相應處理。
UI更新
當修改了View的frame等導致View的圖層發(fā)生了變化或者手動調(diào)用了setNeedsDisplay/setNeedsLayout方法之后就會將這個View標記放進一個全局容器里面,當RunLoop的即將進入休眠或者退出事件回調(diào)時,就會遍歷這個全局容器將UI進行繪制更新。
PerformSelector 的實現(xiàn)原理
- 當調(diào)用 NSObject 的 performSelecter:afterDelay: 后,實際上其內(nèi)部會創(chuàng)建一個 Timer 并添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。
- 當調(diào)用 performSelector:onThread: 時,觸發(fā)的是Source0事件,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。