最近看了很多RunLoop的文章,看完很懵逼,決心整理一下,文章中大部分內(nèi)容都是引用大神們的,但好歹對(duì)自己有個(gè)交代了,花了一個(gè)周天加幾個(gè)晚上熬夜完成的,有個(gè)產(chǎn)出還是很爽的,不多比比了,下面開始吧。
什么是RunLoop?
RunLoop是一個(gè)接收處理異步消息事件的循環(huán),一個(gè)循環(huán)中:等待事件發(fā)生,然后將這個(gè)事件送到能處理它的地方。
RunLoop實(shí)際上是一個(gè)對(duì)象,這個(gè)對(duì)象在循環(huán)中用來處理程序運(yùn)行過程中出現(xiàn)的各種事件(比如說觸摸事件、UI刷新事件、定時(shí)器事件、Selector事件)和消息,從而保持程序的持續(xù)運(yùn)行;而且在沒有事件處理的時(shí)候,會(huì)進(jìn)入睡眠模式,從而節(jié)省CPU資源,提高程序性能。
Event Loop模型偽代碼
int main(int argc, char * argv[]) {
//程序一直運(yùn)行狀態(tài)
while (AppIsRunning) {
//睡眠狀態(tài),等待喚醒事件
id whoWakesMe = SleepForWakingU p();
//得到喚醒事件
id event = GetEvent(whoWakesMe);
//開始處理事件
HandleEvent(event);
}
return 0;
}

- mach kernel屬于蘋果內(nèi)核,RunLoop依靠它實(shí)現(xiàn)了休眠和喚醒而避免了CPU的空轉(zhuǎn)。
- Runloop是基于pthread進(jìn)行管理的,pthread是基于c的跨平臺(tái)多線程操作底層API。它是mach thread的上層封裝(可以參見Kernel Programming Guide),和NSThread一一對(duì)應(yīng)(而NSThread是一套面向?qū)ο蟮腁PI,所以在iOS開發(fā)中我們也幾乎不用直接使用pthread)。

RunLoop的組成
RunLoop構(gòu)成
CFRunLoop對(duì)象可以檢測(cè)某個(gè)task或者dispatch的輸入事件,當(dāng)檢測(cè)到有輸入源事件,CFRunLoop將會(huì)將其加入到線程中進(jìn)行處理。比方說用戶輸入事件、網(wǎng)絡(luò)連接事件、周期性或者延時(shí)事件、異步的回調(diào)等。
RunLoop可以檢測(cè)的事件類型一共有3種,分別是CFRunLoopSource、CFRunLoopTimer、CFRunLoopObserver??梢酝ㄟ^CFRunLoopAddSource, CFRunLoopAddTimer或者CFRunLoopAddObserver添加相應(yīng)的事件類型。
要讓一個(gè)RunLoop跑起來還需要run loop modes,每一個(gè)source, timer和observer添加到RunLoop中時(shí)必須要與一個(gè)模式(CFRunLoopMode)相關(guān)聯(lián)才可以運(yùn)行。
上面是對(duì)于CFRunLoop官方文檔的解釋
RunLoop的主要組成
RunLoop共包含5個(gè)類,但公開的只有Source、Timer、Observer相關(guān)的三個(gè)類。
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

CFRunLoopSourceRef
source是RunLoop的數(shù)據(jù)源(輸入源)的抽象類(protocol),Source有兩個(gè)版本:Source0 和 Source1
- source0:只包含了一個(gè)回調(diào)(函數(shù)指針),使用時(shí),你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個(gè) Source 標(biāo)記為待處理,然后手動(dòng)調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個(gè)事件。處理App內(nèi)部事件,App自己負(fù)責(zé)管理(出發(fā)),如UIEvent(Touch事件等,GS發(fā)起到RunLoop運(yùn)行再到事件回調(diào)到UI)、CFSocketRef。
- Source1:由RunLoop和內(nèi)核管理,由mach_port驅(qū)動(dòng)(特指port-based事件),如CFMachPort、CFMessagePort、NSSocketPort。特別要注意一下Mach port的概念,它是一個(gè)輕量級(jí)的進(jìn)程間通訊的方式,可以理解為它是一個(gè)通訊通道,假如同時(shí)有幾個(gè)進(jìn)程都掛在這個(gè)通道上,那么其它進(jìn)程向這個(gè)通道發(fā)送消息后,這些掛在這個(gè)通道上的進(jìn)程都可以收到相應(yīng)的消息。這個(gè)Port的概念非常重要,因?yàn)樗荝unLoop休眠和被喚醒的關(guān)鍵,它是RunLoop與系統(tǒng)內(nèi)核進(jìn)行消息通訊的窗口。
CFRunLoopTimerRef 是基于時(shí)間的觸發(fā)器,它和 NSTimer 是toll-free bridged 的,可以混用(底層基于使用mk_timer實(shí)現(xiàn))。它受RunLoop的Mode影響(GCD的定時(shí)器不受RunLoop的Mode影響),當(dāng)其加入到 RunLoop 時(shí),RunLoop會(huì)注冊(cè)對(duì)應(yīng)的時(shí)間點(diǎn),當(dāng)時(shí)間點(diǎn)到時(shí),RunLoop會(huì)被喚醒以執(zhí)行那個(gè)回調(diào)。如果線程阻塞或者不在這個(gè)Mode下,觸發(fā)點(diǎn)將不會(huì)執(zhí)行,一直等到下一個(gè)周期時(shí)間點(diǎn)觸發(fā)。
CFRunLoopObserverRef 是觀察者,每個(gè) Observer 都包含了一個(gè)回調(diào)(函數(shù)指針),當(dāng) RunLoop 的狀態(tài)發(fā)生變化時(shí),觀察者就能通過回調(diào)接受到這個(gè)變化??梢杂^測(cè)的時(shí)間點(diǎn)有以下幾個(gè)
enum CFRunLoopActivity {
kCFRunLoopEntry = (1 << 0), // 即將進(jìn)入Loop
kCFRunLoopBeforeTimers = (1 << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1 << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1 << 5), // 即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1 << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1 << 7), // 即將退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 包含上面所有狀態(tài)
};
typedef enum CFRunLoopActivity CFRunLoopActivity;
這里要提一句的是,timer和source1(也就是基于port的source)可以反復(fù)使用,比如timer設(shè)置為repeat,port可以持續(xù)接收消息,而source0在一次觸發(fā)后就會(huì)被runloop移除。
上面的 Source/Timer/Observer 被統(tǒng)稱為 mode item,一個(gè) item 可以被同時(shí)加入多個(gè) mode。但一個(gè) item 被重復(fù)加入同一個(gè) mode 時(shí)是不會(huì)有效果的。如果一個(gè) mode 中一個(gè) item 都沒有,則 RunLoop 會(huì)直接退出,不進(jìn)入循環(huán)。
RunLoop主要處理以下6類事件
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
RunLoop的Mode
CFRunLoopMode 和 CFRunLoop的結(jié)構(gòu)大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
一個(gè)RunLoop包含了多個(gè)Mode,每個(gè)Mode又包含了若干個(gè)Source/Timer/Observer。每次調(diào)用 RunLoop的主函數(shù)時(shí),只能指定其中一個(gè)Mode,這個(gè)Mode被稱作CurrentMode。如果需要切換 Mode,只能退出Loop,再重新指定一個(gè)Mode進(jìn)入。這樣做主要是為了分隔開不同Mode中的Source/Timer/Observer,讓其互不影響。下面是5種Mode
- kCFDefaultRunLoopMode App的默認(rèn)Mode,通常主線程是在這個(gè)Mode下運(yùn)行
- UITrackingRunLoopMode 界面跟蹤Mode,用于ScrollView追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他Mode影響
- UIInitializationRunLoopMode 在剛啟動(dòng)App時(shí)第進(jìn)入的第一個(gè)Mode,啟動(dòng)完成后就不再使用
- GSEventReceiveRunLoopMode 接受系統(tǒng)事件的內(nèi)部Mode,通常用不到
- kCFRunLoopCommonModes 這是一個(gè)占位用的Mode,不是一種真正的Mode
其中kCFDefaultRunLoopMode、UITrackingRunLoopMode是蘋果公開的,其余的mode都是無法添加的。那為何我們又可以這么用呢
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
什么是CommonModes?
一個(gè) Mode 可以將自己標(biāo)記為”Common”屬性(通過將其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每當(dāng) RunLoop 的內(nèi)容發(fā)生變化時(shí),RunLoop 都會(huì)自動(dòng)將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 標(biāo)記的所有Mode里
主線程的 RunLoop 里有 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode,這兩個(gè)Mode都已經(jīng)被標(biāo)記為”Common”屬性。當(dāng)你創(chuàng)建一個(gè)Timer并加到DefaultMode時(shí),Timer會(huì)得到重復(fù)回調(diào),但此時(shí)滑動(dòng)一個(gè) scrollView 時(shí),RunLoop 會(huì)將 mode 切換為TrackingRunLoopMode,這時(shí)Timer就不會(huì)被回調(diào),并且也不會(huì)影響到滑動(dòng)操作。
如果想讓scrollView滑動(dòng)時(shí)Timer可以正常調(diào)用,一種辦法就是手動(dòng)將這個(gè) Timer 分別加入這兩個(gè) Mode。另一種方法就是將 Timer 加入到CommonMode 中。
怎么將事件加入到CommonMode?
我們調(diào)用上面的代碼將 Timer 加入到CommonMode 時(shí),但實(shí)際并沒有 CommonMode,其實(shí)系統(tǒng)將這個(gè) Timer 加入到頂層的 RunLoop 的 commonModeItems 中。commonModeItems 會(huì)被 RunLoop 自動(dòng)更新到所有具有”Common”屬性的 Mode 里去。
這一步其實(shí)是系統(tǒng)幫我們將Timer加到了kCFRunLoopDefaultMode和UITrackingRunLoopMode中。
在項(xiàng)目中最常用的就是設(shè)置NSTimer的Mode,比較簡(jiǎn)單這里就不說了。
RunLoop運(yùn)行機(jī)制

當(dāng)你調(diào)用 CFRunLoopRun() 時(shí),線程就會(huì)一直停留在這個(gè)循環(huán)里;直到超時(shí)或被手動(dòng)停止,該函數(shù)才會(huì)返回。每次線程運(yùn)行RunLoop都會(huì)自動(dòng)處理之前未處理的消息,并且將消息發(fā)送給觀察者,讓事件得到執(zhí)行。RunLoop運(yùn)行時(shí)首先根據(jù)modeName找到對(duì)應(yīng)mode,如果mode里沒有source/timer/observer,直接返回。
/// 用DefaultMode啟動(dòng)
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode啟動(dòng),允許設(shè)置RunLoop超時(shí)時(shí)間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的實(shí)現(xiàn)
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根據(jù)modeName找到對(duì)應(yīng)mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里沒有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即將進(jìn)入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 內(nèi)部函數(shù),進(jìn)入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 執(zhí)行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 觸發(fā) Source0 (非port) 回調(diào)。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 執(zhí)行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 處于 ready 狀態(tài),直接處理這個(gè) Source1 然后跳轉(zhuǎn)去處理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的線程即將進(jìn)入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息。線程將進(jìn)入休眠, 直到被下面某一個(gè)事件喚醒。
/// ? 一個(gè)基于 port 的Source 的事件。
/// ? 一個(gè) Timer 到時(shí)間了
/// ? RunLoop 自身的超時(shí)時(shí)間到了
/// ? 被其他什么調(diào)用者手動(dòng)喚醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,處理消息。
handle_msg:
/// 9.1 如果一個(gè) Timer 到時(shí)間了,觸發(fā)這個(gè)Timer的回調(diào)。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,執(zhí)行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一個(gè) Source1 (基于port) 發(fā)出事件了,處理這個(gè)事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 執(zhí)行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 進(jìn)入loop時(shí)參數(shù)說處理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出傳入?yún)?shù)標(biāo)記的超時(shí)時(shí)間了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部調(diào)用者強(qiáng)制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一個(gè)都沒有了
retVal = kCFRunLoopRunFinished;
}
/// 如果沒超時(shí),mode里沒空,loop也沒被停止,那繼續(xù)loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即將退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
RunLoop的掛起和喚醒
RunLoop的掛起
RunLoop的掛起是通過_CFRunLoopServiceMachPort —call—> mach_msg —call—> mach_msg_trap這個(gè)調(diào)用順序來告訴內(nèi)核RunLoop監(jiān)聽哪個(gè)mach_port(上面提到的消息通道),然后等待事件的發(fā)生(等待與InputSource、Timer描述內(nèi)容相關(guān)的事件),這樣內(nèi)核就把RunLoop掛起了,即RunLoop休眠了。
RunLoop的喚醒
這接種情況下會(huì)被喚醒
- 存在Source0被標(biāo)記為待處理,系統(tǒng)調(diào)用CFRunLoopWakeUp喚醒線程處理事件
- 定時(shí)器時(shí)間到了
- RunLoop自身的超時(shí)時(shí)間到了
- RunLoop外部調(diào)用者喚醒
當(dāng)RunLoop被掛起后,如果之前監(jiān)聽的事件發(fā)生了,由另一個(gè)線程(或另一個(gè)進(jìn)程中的某個(gè)線程)向內(nèi)核發(fā)送這個(gè)mach_port的msg后,trap狀態(tài)被喚醒,RunLoop繼續(xù)運(yùn)行
處理事件
- 如果一個(gè) Timer 到時(shí)間了,觸發(fā)這個(gè)Timer的回調(diào)
- 如果有dispatch到main_queue的block,執(zhí)行block
- 如果一個(gè) Source1 發(fā)出事件了,處理這個(gè)事件
事件處理完成進(jìn)行判斷
- 進(jìn)入loop時(shí)傳入?yún)?shù)指明處理完事件就返回(stopAfterHandle)
- 超出傳入?yún)?shù)標(biāo)記的超時(shí)時(shí)間(timeout)
- 被外部調(diào)用者強(qiáng)制停止__CFRunLoopIsStopped(runloop)
- source/timer/observer 全都空了__CFRunLoopModeIsEmpty(runloop, currentMode)
RunLoop 的底層實(shí)現(xiàn)
關(guān)于這個(gè)大家可以看ibireme的深入理解RunLoop一文,我這里選擇一些覺得比較重要又不是那么難懂的。
Mach消息發(fā)送機(jī)制看這篇文章Mach消息發(fā)送機(jī)制
為了實(shí)現(xiàn)消息的發(fā)送和接收,mach_msg() 函數(shù)實(shí)際上是調(diào)用了一個(gè) Mach 陷阱 (trap),即函數(shù)mach_msg_trap(),陷阱這個(gè)概念在 Mach 中等同于系統(tǒng)調(diào)用。當(dāng)你在用戶態(tài)調(diào)用 mach_msg_trap() 時(shí)會(huì)觸發(fā)陷阱機(jī)制,切換到內(nèi)核態(tài);內(nèi)核態(tài)中內(nèi)核實(shí)現(xiàn)的 mach_msg() 函數(shù)會(huì)完成實(shí)際的工作,如下圖:

RunLoop 的核心就是一個(gè) mach_msg() (見上面代碼的第7步),RunLoop 調(diào)用這個(gè)函數(shù)去接收消息,如果沒有別人發(fā)送 port 消息過來,內(nèi)核會(huì)將線程置于等待狀態(tài)。例如你在模擬器里跑起一個(gè) iOS 的 App,然后在 App 靜止時(shí)點(diǎn)擊暫停,你會(huì)看到主線程調(diào)用棧是停留在 mach_msg_trap() 這個(gè)地方。
RunLoop和線程
RunLoop和線程是息息相關(guān)的,我們知道線程的作用是用來執(zhí)行特定的一個(gè)或多個(gè)任務(wù),但是在默認(rèn)情況下,線程執(zhí)行完之后就會(huì)退出,就不能再執(zhí)行任務(wù)了。這時(shí)我們就需要采用一種方式來讓線程能夠處理任務(wù),并不退出。所以,我們就有了RunLoop。
iOS開發(fā)中能遇到兩個(gè)線程對(duì)象: pthread_t和NSThread,pthread_t和NSThread 是一一對(duì)應(yīng)的。比如,你可以通過 pthread_main_thread_np()或 [NSThread mainThread]來獲取主線程;也可以通過pthread_self()或[NSThread currentThread]來獲取當(dāng)前線程。CFRunLoop 是基于 pthread 來管理的。
線程與RunLoop是一一對(duì)應(yīng)的關(guān)系(對(duì)應(yīng)關(guān)系保存在一個(gè)全局的Dictionary里),線程創(chuàng)建之后是沒有RunLoop的(主線程除外),RunLoop的創(chuàng)建是發(fā)生在第一次獲取時(shí),銷毀則是在線程結(jié)束的時(shí)候。只能在當(dāng)前線程中操作當(dāng)前線程的RunLoop,而不能去操作其他線程的RunLoop。
蘋果不允許直接創(chuàng)建RunLoop,但是可以通過[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()來獲?。ㄈ绻麤]有就會(huì)自動(dòng)創(chuàng)建一個(gè))。
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時(shí)的鎖
static CFSpinLock_t loopsLock;
/// 獲取一個(gè) pthread 對(duì)應(yīng)的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進(jìn)入時(shí),初始化全局Dic,并先為主線程創(chuàng)建一個(gè) RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 里獲取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時(shí),創(chuàng)建一個(gè)
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注冊(cè)一個(gè)回調(diào),當(dāng)線程銷毀時(shí),順便也銷毀其對(duì)應(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());
}
開發(fā)過程中需要RunLoop時(shí),則需要手動(dòng)創(chuàng)建和運(yùn)行RunLoop(尤其是在子線程中, 主線程中的Main RunLoop除外),我看到別人舉了這么個(gè)例子,很有意思
調(diào)用[NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:]帶有schedule的方法簇來啟動(dòng)Timer.
此方法會(huì)創(chuàng)建Timer并把Timer放到當(dāng)前線程的RunLoop中,隨后RunLoop會(huì)在Timer設(shè)定的時(shí)間點(diǎn)回調(diào)Timer綁定的selector或Invocation。但是,在主線程和子線程中調(diào)用此方法的效果是有差異的,即在主線程中調(diào)用scheduledTimer方法時(shí)timer可以在設(shè)定的時(shí)間點(diǎn)觸發(fā),但是在子線程里則不能觸發(fā)。這是因?yàn)樽泳€程中沒有創(chuàng)建RunLoop且更沒有啟動(dòng)RunLoop,而主線程中的RunLoop默認(rèn)是創(chuàng)建好的且一直運(yùn)行著。所以,子線程中需要像下面這樣調(diào)用。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] run];
});
那為什么下面這樣調(diào)用同樣不會(huì)觸發(fā)Timer呢?
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSRunLoop currentRunLoop] run];
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
});
我的分析是:scheduledTimerWithTimeInterval內(nèi)部在向RunLoop傳遞Timer時(shí)是調(diào)用與線程實(shí)例相關(guān)的單例方法[NSRunLoop currentRunLoop]來獲取RunLoop實(shí)例的,即RunLoop實(shí)例不存在就創(chuàng)建一個(gè)與當(dāng)前線程相關(guān)的RunLoop并把Timer傳遞到RunLoop中,存在則直接傳Timer到RunLoop中即可。而在RunLoop開始運(yùn)行后再向其傳遞Timer時(shí),由于dispatch_async代碼塊里的兩行代碼是順序執(zhí)行,[[NSRunLoop currentRunLoop] run]是一個(gè)沒有結(jié)束時(shí)間的RunLoop,無法執(zhí)行到“[NSTimer scheduledTimerWithTimeInterval:…”這一行代碼,Timer也就沒有被加到當(dāng)前RunLoop中,所以更不會(huì)觸發(fā)Timer了。
蘋果用 RunLoop 實(shí)現(xiàn)的功能
AutoreleasePool
App啟動(dòng)之后,系統(tǒng)啟動(dòng)主線程并創(chuàng)建了RunLoop,在main thread中注冊(cè)了兩個(gè)observer,回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()
第一個(gè)Observer監(jiān)視的事件
- 即將進(jìn)入Loop(kCFRunLoopEntry),其回調(diào)內(nèi)會(huì)調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動(dòng)釋放池。其order是-2147483647,優(yōu)先級(jí)最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。
第二個(gè)Observer監(jiān)視了兩個(gè)事件
準(zhǔn)備進(jìn)入休眠(kCFRunLoopBeforeWaiting),此時(shí)調(diào)用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 來釋放舊的池并創(chuàng)建新的池。
即將退出Loop(kCFRunLoopExit)此時(shí)調(diào)用 _objc_autoreleasePoolPop()釋放自動(dòng)釋放池。這個(gè) Observer的order是2147483647,確保池子釋放在所有回調(diào)之后。
我們知道AutoRelease對(duì)象是被AutoReleasePool管理的,那么AutoRelease對(duì)象在什么時(shí)候被回收呢?
第一種情況:在我們自己寫的for循環(huán)或線程體里,我們都習(xí)慣用AutoReleasePool來管理一些臨時(shí)變量的autorelease,使得在for循環(huán)或線程結(jié)束后回收AutoReleasePool的時(shí)候來回收AutoRelease臨時(shí)變量。
另一種情況:我們?cè)谥骶€程里創(chuàng)建了一些AutoRelease對(duì)象,這些對(duì)象可不能指望在回收Main AutoReleasePool時(shí)才被回收,因?yàn)锳pp一直運(yùn)行的過程中Main AutoReleasePool是不會(huì)被回收的。那么這種AutoRelease對(duì)象的回收就依賴Main RunLoop的運(yùn)行狀態(tài),Main RunLoop的Observer會(huì)在Main RunLoop結(jié)束休眠被喚醒時(shí)(kCFRunLoopAfterWaiting狀態(tài))通知UIKit,UIKit收到這一通知后就會(huì)調(diào)用_CFAutorleasePoolPop方法來回收主線程中的所有AutoRelease對(duì)象。
在主線程中執(zhí)行代碼一般都是寫在事件回調(diào)或Timer回調(diào)中的,這些回調(diào)都被加入了main thread的自動(dòng)釋放池中,所以在ARC模式下我們不用關(guān)心對(duì)象什么時(shí)候釋放,也不用去創(chuàng)建和管理pool。(如果事件不在主線程中要注意創(chuàng)建自動(dòng)釋放池,否則可能會(huì)出現(xiàn)內(nèi)存泄漏)。
NSTimer(timer觸發(fā))
上文說到了CFRunLoopTimerRef,其實(shí)NSTimer的原型就是CFRunLoopTimerRef。一個(gè)Timer注冊(cè) RunLoop 之后,RunLoop 會(huì)為這個(gè)Timer的重復(fù)時(shí)間點(diǎn)注冊(cè)好事件。有兩點(diǎn)需要注意:
- 但是需要注意的是RunLoop為了節(jié)省資源,并不會(huì)在非常準(zhǔn)確的時(shí)間點(diǎn)回調(diào)這個(gè)Timer。Timer 有個(gè)屬性叫做 Tolerance (寬容度),標(biāo)示了當(dāng)時(shí)間點(diǎn)到后,容許有多少最大誤差。這個(gè)誤差默認(rèn)為0,我們可以手動(dòng)設(shè)置這個(gè)誤差。文檔最后還強(qiáng)調(diào)了,為了防止時(shí)間點(diǎn)偏移,系統(tǒng)有權(quán)力給這個(gè)屬性設(shè)置一個(gè)值無論你設(shè)置的值是多少,即使RunLoop 模式正確,當(dāng)前線程并不阻塞,系統(tǒng)依然可能會(huì)在 NSTimer 上加上很小的的容差。
- 我們?cè)谀膫€(gè)線程調(diào)用 NSTimer 就必須在哪個(gè)線程終止
在RunLoop的Mode中也有說到,NSTimer使用的時(shí)候注意Mode,比如我之前開發(fā)時(shí)候用NSTimer寫一個(gè)Banner圖片輪播框架,如果不設(shè)置Timer的Mode為commonModes那么在滑動(dòng)TableView的時(shí)候Banner就停止輪播
DispatchQueue.global().async {
// 非主線程不能使用 Timer.scheduledTimer進(jìn)行初始化
// self.timer = Timer.scheduledTimer(timeInterval: 6.0, target: self, selector: #selector(TurnPlayerView.didTurnPlay), userInfo: nil, repeats: false)
if #available(iOS 10.0, *) {
self.timer = Timer(timeInterval: 6.0, repeats: true, block: { (timer) in
self.setContentOffset(CGPoint(x: self.frame.width*2, y: self.contentOffset.y), animated: true)
})
} else {
// Fallback on earlier versions
}
RunLoop.main.add(self.timer!, forMode: RunLoopMode.commonModes)
}
和GCD的關(guān)系
- RunLoop底層用到GCD
- RunLoop與GCD并沒有直接關(guān)系,但當(dāng)GCD使用到main_queue時(shí)才有關(guān)系,如下:
//實(shí)驗(yàn)GCD Timer 與 Runloop的關(guān)系,只有當(dāng)dispatch_get_main_queue時(shí)才與RunLoop有關(guān)系
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"GCD Timer...");
});
當(dāng)調(diào)用 dispatch_async(dispatch_get_main_queue(), block) 時(shí),libDispatch 會(huì)向主線程的 RunLoop 發(fā)送消息,RunLoop會(huì)被喚醒,并從消息中取得這個(gè) block,并在回調(diào) CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里執(zhí)行這個(gè) block。但這個(gè)邏輯僅限于 dispatch 到主線程,dispatch 到其他線程仍然是由 libDispatch 處理的。同理,GCD的dispatch_after在dispatch到main_queue時(shí)的timer機(jī)制才與RunLoop相關(guān)。
PerformSelecter
NSObject的performSelecter:afterDelay: 實(shí)際上其內(nèi)部會(huì)創(chuàng)建一個(gè) Timer 并添加到當(dāng)前線程的 RunLoop 中。所以如果當(dāng)前線程沒有 RunLoop,則這個(gè)方法會(huì)失效。
NSObject的performSelector:onThread: 實(shí)際上其會(huì)創(chuàng)建一個(gè) Timer 加到對(duì)應(yīng)的線程去,同樣的,如果對(duì)應(yīng)線程沒有 RunLoop 該方法也會(huì)失效。
其實(shí)這種方式有種說法也叫創(chuàng)建常駐線程(內(nèi)存),AFNetworking也用到這種技法。舉個(gè)例子,如果把RunLoop去掉,那么test方法就不會(huì)執(zhí)行。
class SecondViewController: UIViewController {
var thread: Thread!
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.red
thread = Thread.init(target: self, selector: #selector(SecondViewController.run), object: nil)
thread.start()
}
@objc func run() {
print("run -- ")
RunLoop.current.add(Port(), forMode: .defaultRunLoopMode)
RunLoop.current.run()
}
@objc func test() {
print("test -- \(Thread.current)")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// self.test()
self.perform(#selector(SecondViewController.test), on: thread, with: nil, waitUntilDone: false)
}
}
網(wǎng)絡(luò)請(qǐng)求
iOS中的網(wǎng)絡(luò)請(qǐng)求接口自下而上有這么幾層

其中CFSocket和CFNetwork偏底層,早些時(shí)候比較知名的網(wǎng)絡(luò)框架AFNetworking是基于NSURLConnection編寫的,iOS7之后新增了NSURLSession,NSURLSession的底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程),之后AFNetworking和Alamofire就是基于它封裝的了。

通常使用 NSURLConnection 時(shí),會(huì)傳入一個(gè) Delegate,當(dāng)調(diào)用了 [connection start] 后,這個(gè) Delegate 就會(huì)不停收到事件回調(diào)。實(shí)際上,start 這個(gè)函數(shù)的內(nèi)部會(huì)獲取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4個(gè) Source0 (即需要手動(dòng)觸發(fā)的Source)。CFMultiplexerSource 是負(fù)責(zé)各種 Delegate 回調(diào)的,CFHTTPCookieStorage 是處理各種 Cookie 的。
開始網(wǎng)絡(luò)傳輸時(shí),NSURLConnection 創(chuàng)建了兩個(gè)新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。
其中 CFSocket 線程是處理底層 socket 連接的,NSURLConnectionLoader中的RunLoop通過一些基于mach port的Source1接收來自底層CFSocket的通知。當(dāng)收到通知后,其會(huì)在合適的時(shí)機(jī)向CFMultiplexerSource等Source0發(fā)送通知,同時(shí)喚醒Delegate線程的RunLoop來讓其處理這些通知。CFMultiplexerSource會(huì)在Delegate線程的RunLoop對(duì)Delegate執(zhí)行實(shí)際的回調(diào)。
事件響應(yīng)
蘋果注冊(cè)了一個(gè) Source1 (基于 mach port 的) 用來接收系統(tǒng)事件,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()。
當(dāng)一個(gè)硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個(gè) IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進(jìn)程。
觸摸事件其實(shí)是Source1接收系統(tǒng)事件后在回調(diào) __IOHIDEventSystemClientQueueCallback()內(nèi)觸發(fā)的 Source0,Source0 再觸發(fā)的 _UIApplicationHandleEventQueue()。source0一定是要喚醒runloop及時(shí)響應(yīng)并執(zhí)行的,如果runloop此時(shí)在休眠等待系統(tǒng)的 mach_msg事件,那么就會(huì)通過source1來喚醒runloop執(zhí)行。
_UIApplicationHandleEventQueue() 會(huì)把 IOHIDEvent 處理并包裝成 UIEvent 進(jìn)行處理或分發(fā),其中包括識(shí)別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等。

手勢(shì)識(shí)別
當(dāng)上面的 _UIApplicationHandleEventQueue() 識(shí)別了一個(gè)手勢(shì)時(shí),其首先會(huì)調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷。隨后系統(tǒng)將對(duì)應(yīng)的 UIGestureRecognizer 標(biāo)記為待處理。
蘋果注冊(cè)了一個(gè) Observer 監(jiān)測(cè) BeforeWaiting (Loop即將進(jìn)入休眠) 事件,這個(gè)Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver(),其內(nèi)部會(huì)獲取所有剛被標(biāo)記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)。
當(dāng)有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時(shí),這個(gè)回調(diào)都會(huì)進(jìn)行相應(yīng)處理。
UI更新
Core Animation 在 RunLoop 中注冊(cè)了一個(gè) Observer 監(jiān)聽 BeforeWaiting(即將進(jìn)入休眠) 和 Exit (即將退出Loop) 事件 。當(dāng)在操作 UI 時(shí),比如改變了 Frame、更新了 UIView/CALayer 的層次時(shí),或者手動(dòng)調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個(gè) UIView/CALayer 就被標(biāo)記為待處理,并被提交到一個(gè)全局的容器去。當(dāng)Oberver監(jiān)聽的事件到來時(shí),回調(diào)執(zhí)行函數(shù)中會(huì)遍歷所有待處理的UIView/CAlayer 以執(zhí)行實(shí)際的繪制和調(diào)整,并更新 UI 界面。
如果此處有動(dòng)畫,通過 DisplayLink 穩(wěn)定的刷新機(jī)制會(huì)不斷的喚醒runloop,使得不斷的有機(jī)會(huì)觸發(fā)observer回調(diào),從而根據(jù)時(shí)間來不斷更新這個(gè)動(dòng)畫的屬性值并繪制出來。
函數(shù)內(nèi)部的調(diào)用棧
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
繪圖和動(dòng)畫有兩種處理的方式:CPU(中央處理器)和GPU(圖形處理器)
CPU: CPU 中計(jì)算顯示內(nèi)容,比如視圖的創(chuàng)建、布局計(jì)算、圖片解碼、文本繪制等
GPU: GPU 進(jìn)行變換、合成、渲染.
關(guān)于CADisplayLink的描述有兩種
CADisplayLink 是一個(gè)和屏幕刷新率一致的定時(shí)器(但實(shí)際實(shí)現(xiàn)原理更復(fù)雜,和 NSTimer 并不一樣,其內(nèi)部實(shí)際是操作了一個(gè) Source)。如果在兩次屏幕刷新之間執(zhí)行了一個(gè)長(zhǎng)任務(wù),那其中就會(huì)有一幀被跳過去(和 NSTimer 相似),造成界面卡頓的感覺。在快速滑動(dòng)TableView時(shí),即使一幀的卡頓也會(huì)讓用戶有所察覺。
CADisplayLink是一個(gè)執(zhí)行頻率(fps)和屏幕刷新相同(可以修改preferredFramesPerSecond改變刷新頻率)的定時(shí)器,它也需要加入到RunLoop才能執(zhí)行。與NSTimer類似,CADisplayLink同樣是基于CFRunloopTimerRef實(shí)現(xiàn),底層使用mk_timer(可以比較加入到RunLoop前后RunLoop中timer的變化)。和NSTimer相比它精度更高(盡管NSTimer也可以修改精度),不過和NStimer類似的是如果遇到大任務(wù)它仍然存在丟幀現(xiàn)象。通常情況下CADisaplayLink用于構(gòu)建幀動(dòng)畫,看起來相對(duì)更加流暢,而NSTimer則有更廣泛的用處。
不管怎么樣CADisplayLink和NSTimer是有很大不同的,詳情可以參考這篇文章CADisplayLink
ibireme根據(jù)CADisplayLink的特性寫了個(gè)FPS指示器YYFPSLabel,代碼非常少
原理是這樣的:既然CADisplayLink可以以屏幕刷新的頻率調(diào)用指定selector,而且iOS系統(tǒng)中正常的屏幕刷新率為60Hz(60次每秒),所以使用 CADisplayLink 的 timestamp 屬性,配合 timer 的執(zhí)行次數(shù)計(jì)算得出FPS數(shù)
參考文章
深入理解RunLoop
iOS 事件處理機(jī)制與圖像渲染過程
RunLoop學(xué)習(xí)筆記(一) 基本原理介紹
iOS刨根問底-深入理解RunLoop
【iOS程序啟動(dòng)與運(yùn)轉(zhuǎn)】- RunLoop個(gè)人小結(jié)
RunLoop的前世今生
Runloop知識(shí)樹