一、什么是RunLoop?
-
RunLoop就是運行循環(huán),在程序運行過程中循環(huán)做一些事情,在很多地方都會應(yīng)用到,例如:定時器、PerformSelector、GCD Async Main Queue、事件響應(yīng)、手勢識別、界面刷新、網(wǎng)絡(luò)請求、AutoreleasePool
-
- 如果沒有
RunLoop的話,例如以下代碼,在執(zhí)行完第13行代碼后,會即將退出程序
- 如果沒有

- 如果有了
RunLoop的話,例如以下代碼,程序并不會馬上退出,而是保持運行狀態(tài)
- 如果有了

- 我們見到的程序長時間保持活躍狀態(tài)都是
RunLoop的功勞,UIApplicationMain()會創(chuàng)建主線程,主線程內(nèi)部會主動開啟一個RunLoop,而RunLoop本質(zhì)上就是一個do-while循環(huán),只要條件滿足,就會不停的循環(huán),進而程序一直保持運行的狀態(tài)。RunLoop源碼如下所示:
- 我們見到的程序長時間保持活躍狀態(tài)都是
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(),kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
-
- RunLoop的作用有:
保持程序處于持續(xù)運行
處理App中的各種事件,例如觸摸事件、定時器事件
節(jié)省CPU資源、提高程序性能:該做事的時候做事
二、RunLoop對象
- iOS中有兩套API來訪問和使用RunLoop:
Foundation中的NSRunLoop、Core Foundation中的CFRunLoopRef,其中的NSRunLoop和CFRunLoopRef都代表著RunLoop對象,NSRunLoop是基于CFRunLoopRef的一層OC包裝
- iOS中有兩套API來訪問和使用RunLoop:
-
CFRunLoop是開源的,地址是:https://opensource.apple.com/tarballs/CF/,CFRunLoopRef提供了兩個自動獲取RunLoop的函數(shù):CFRunLoopGetMain()、CFRunLoopGetCurrent(),其內(nèi)部邏輯如下:
-
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
/// 獲取一個 pthread 對應(yīng)的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進入時,初始化全局Dic,并先為主線程創(chuàng)建一個 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 里獲取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時,創(chuàng)建一個
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注冊一個回調(diào),當線程銷毀時,順便也銷毀其對應(yīng)的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
-
3.從上述源代碼可以看出,
RunLoop與線程之間的關(guān)系如下:每條線程都有唯一的一個與之對應(yīng)的RunLoop對象
RunLoop保存在一個全局的Dictionary里,線程作為Key,RunLoop作為Value
線程剛創(chuàng)建時并沒有RunLoop對象,RunLoop會在第一次獲取它時創(chuàng)建
RunLoop會在線程結(jié)束時銷毀
主線程的RunLoop默認已經(jīng)自動創(chuàng)建了,而子線程默認沒有開啟RunLoop
-
- 獲取
RunLoop對象的方法:
- Foundation框架:
- 獲取
[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
- Core Foundation框架:
CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象
三、RunLoop對外的接口
- 在 CoreFoundation 里面關(guān)于 RunLoop 有5個類:
CFRunLoopRef、CFRunLoopModeRef、CFRunLoopSourceRef、CFRunLoopTimerRef、CFRunLoopObserverRef,它們之間的關(guān)系如下所示:
RunLoop
- 在 CoreFoundation 里面關(guān)于 RunLoop 有5個類:
-
- 從上圖,我們可以看出來,一個RunLoop里面可以有多個mode,每個mode又可以多個source,observer,timer。可是每次RunLoop只能指定一個mode運行,如果想要切換mode,就必須先退出RunLoop,然后重新指定mode運行,這樣做的目的就是避免mode之間相互影響。
注意: 如果Mode里沒有任何Souce0、Souce1、Timer、Observer,RunLoop會立馬退出
下面我們就詳細說說關(guān)于RunLoop的五個類
-
CFRunLoopRef是一個叫做__CFRunLoop的結(jié)構(gòu)體,其內(nèi)部結(jié)構(gòu)如下所示,內(nèi)部存放著可用的Mode集合、線程、當前的運行Mode
__CFRunLoop內(nèi)部結(jié)構(gòu)
-
-
CFRunLoopModeRef代表了RunLoop的運行模式,底層是__CFRunLoopMode的結(jié)構(gòu)體,內(nèi)部結(jié)構(gòu)如下所示,內(nèi)部存放著souce0、souce1、observers、timers
__CFRunLoopMode結(jié)構(gòu)體.png
-
創(chuàng)建RunLoop時,系統(tǒng)默認注冊了五種mode:
- (1). ```kCFRunLoopDefaultMode```: 默認 mode,通常主線程在這個 mode 下運行
- (2). ```UITrackingRunLoopMode```: 追蹤mode,保證Scrollview滑動順暢不受其他 mode 影響
- (3). ```UIInitializationRunLoopMode```: 啟動程序后的過渡mode,啟動完成后就不再使用
- (4). ```GSEventReceiveRunLoopMode```: Graphic相關(guān)事件的mode,通常用不到
- (5). ```kCFRunLoopCommonModes```: 占位用的mode,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用
-
-
CFRunLoopSourceRef是事件產(chǎn)生的地方,分為Source0和Source1兩種:
Source0: 只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件。使用時,你需要先調(diào)用CFRunLoopSourceSignal(source),將這個Source標記為待處理,然后手動調(diào)用CFRunLoopWakeUp(runloop)來喚醒RunLoop,讓其處理這個事件Source1: 包含了一個mach_port和一個回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息。這種Source能主動喚醒RunLoop的線程
-
-
CFRunLoopTimerRef是基于時間的觸發(fā)器,它和NSTimer是toll-free bridged的,可以混用。其包含一個時間長度和一個回調(diào)(函數(shù)指針)。當其加入到 RunLoop 時,RunLoop會注冊對應(yīng)的時間點,當時間點到時,RunLoop會被喚醒以執(zhí)行那個回調(diào)
-
-
CFRunLoopObserverRef是觀察者,每個Observer都包含了一個回調(diào)(函數(shù)指針),當RunLoop的狀態(tài)發(fā)生變化時,觀察者就能通過回調(diào)接受到這個變化。可以觀測的時間點有以下幾個:
-
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
};
- 可以通過該添加
Observer監(jiān)聽RunLoop的狀態(tài),如下所示:
- 可以通過該添加

四、RunLoop的運行邏輯
- RunLoop的運行邏輯如下所示:

- 上圖的運行邏輯文字描述是這樣的:

-
-
RunLoop 的核心就是一個
mach_msg(),當一個RunLoop處理完事件后,即將進入休眠時,會經(jīng)歷下面幾步:
(1). 指定一個將來喚醒自己的
mach_port端口(2). 調(diào)用
mach_msg來監(jiān)聽這個端口,保持mach_msg_trap狀態(tài)(3). 由另一個線程(比如有可能有一個專門處理鍵盤輸入事件的loop在后臺一直運行)向內(nèi)核發(fā)送這個端口的
msg后,mach_msg_trap狀態(tài)被喚醒,RunLoop繼續(xù)運行
-
RunLoop 的核心就是一個
五、RunLoop的應(yīng)用
-
線程包活,例如:
AFNetworking的老版本就使用了線程包活,讓子線程永遠的活著,其源碼如下所示:
-
線程包活,例如:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
- 主線程的
RunLoop會自動創(chuàng)建,子線程的RunLoop默認不創(chuàng)建,在子線程中調(diào)用NSRunLoop.current獲取RunLoop對象的時候,就會創(chuàng)建RunLoop
- 主線程的
-
RunLoop啟動前內(nèi)部必須要有至少一個Timer/Source,所以AFNetworking在[runLoop run]之前先創(chuàng)建了一個新的NSMachPort添加進去了。通常情況下,調(diào)用者需要持有這個NSMachPort (mach_port)并在外部線程通過這個port發(fā)送消息到loop內(nèi);但此處添加port只是為了讓RunLoop不至于退出,并沒有用于實際的發(fā)送消息。
-
-
-
事件響應(yīng),當一個事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 SpringBoard 接收,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進程,然后觸發(fā)進程的Source1的
_UIApplicationHandleEventQueue()回調(diào),_UIApplicationHandleEventQueue()會把IOHIDEvent處理并包裝成UIEvent進行處理或分發(fā),其中包括識別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等,通常事件比如 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調(diào)中完成的。
- 當我們點擊某個按鈕時,通過打斷點,可以看出來函數(shù)調(diào)用棧是通過
Source0過來的 ,如下所示,與我們上面描述的似乎有些不同,這是因為:事件確實是由Source1接受的,在其回調(diào)里會觸發(fā)Souce0,Source0 再觸發(fā)的_UIApplicationHandleEventQueue()進行事件分發(fā)和處理,所以UIButton事件看到是在 Source0 內(nèi)的。
-
事件響應(yīng),當一個事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 SpringBoard 接收,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進程,然后觸發(fā)進程的Source1的

-
-
界面刷新,當在操作 UI 時,比如改變了 Frame、更新了
UIView/CALayer的層次時,或者手動調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標記為待處理,并被提交到一個全局的容器去。
- 蘋果注冊了一個
Observer監(jiān)聽BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop)事件,回調(diào)去執(zhí)行一個很長的函數(shù):_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv(),這個函數(shù)里會遍歷所有待處理的UIView/CALayer以執(zhí)行實際的繪制和調(diào)整,并更新 UI 界面。
-
界面刷新,當在操作 UI 時,比如改變了 Frame、更新了
六、面試題
- 講講 RunLoop,項目中有用到嗎?
- runloop內(nèi)部實現(xiàn)邏輯?
- runloop和線程的關(guān)系?
- timer 與 runloop 的關(guān)系?
- 程序中添加每3秒響應(yīng)一次的NSTimer,當拖動tableview時timer可能無法響應(yīng)要怎么解決?
- runloop 是怎么響應(yīng)用戶操作的, 具體流程是什么樣的?
- 說說runLoop的幾種狀態(tài)
- runloop的mode作用是什么?


