RunLoop 是什么?
RunLoop 是和線程緊密相關(guān)的一個(gè)基礎(chǔ)組件疹尾。顧名思議就是循環(huán)運(yùn)行秘豹。按照 OC 的思路,RunLoop 其實(shí)就是一個(gè)對象术浪,這個(gè)對象管理了其需要處理的事件和消息并提供一個(gè)入口函數(shù)來循環(huán)執(zhí)行事件瓢对。平常,一般的 while 循環(huán)會讓 CPU 處于忙等狀態(tài)胰苏,而 RunLoop 則是一種“閑等”,當(dāng)沒有事件時(shí)醇疼,RunLoop 會進(jìn)入休眠狀態(tài)硕并,有事件發(fā)生時(shí), RunLoop 會找對應(yīng)的 Handler 處理事件秧荆。
邏輯代碼如下:
+ (void) loop {
[self initialize];
do {
id message = [self get_next_Message];
[self process_message:message];
} while (message != quit);
}
RunLoop 可以保持程序的正常運(yùn)行倔毙,可以處理 APP 的各種事件(比如觸摸、定時(shí)器等)乙濒。同時(shí)也節(jié)省了 CPU 的資源陕赃、 提高性能。
OS X/iOS 系統(tǒng)中颁股,提供了兩個(gè)對象:
- CFRunLoopRef : 在 Core Foundation 框架內(nèi)么库,提供了純 C函數(shù)且線程安全的 API
- NSRunLoop: 基于 CFRunLoop 的封裝,提供了面向?qū)ο蟮?API甘有,但這些線程是不安全的诉儒。
這兩類 API 都可以訪問和使用 RunLoop,但相對來說亏掀,CFRunLoopRef 的性能更高忱反。
首先泛释,看一下官方 RunLoop 結(jié)構(gòu)圖(下圖的Input Source Port 對應(yīng)的是 Source1)
注意:圖中出現(xiàn)的 Input Source 和 Timer Source 都是 RunLoop 事件的來源。但是不同之處在于所有的 Timer 都共用一個(gè)端口 "Mode Timer Port" ,而每個(gè)Source1 都有不同的對應(yīng)端口温算。
RunLoop 與線程
CFRunLoop 是基于 pthread 來管理的怜校。每個(gè)線程都有一個(gè)對應(yīng)的 RunLoop 對象。它們之間的關(guān)系保存在一個(gè)全局的 Dictionary 中注竿。
蘋果不允許直接創(chuàng)建 RunLoop,它提供了 CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
這兩個(gè) API 來獲取 RunLoop 對象韭畸。
主線程的 RunLoop 會在應(yīng)用啟動的時(shí)候完成啟動,其他線程的 RunLoop 默認(rèn)并不會開啟蔓搞,需要我們主動獲取胰丁。
RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時(shí),銷毀是在線程結(jié)束喂分。并且你只能在一個(gè)線程的內(nèi)部獲取其 RunLoop(主線程外)锦庸。
RunLoop 相關(guān)類
在 Core Foundation 里有5個(gè)關(guān)于 RunLoop 的類:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRed
其中,CFRunLoopModeRef 沒有對外暴露蒲祈,只是通過 CFRunLoopRef 的接口進(jìn)行了封裝甘萧。它們之間的關(guān)系如下圖:
一個(gè) RunLoop 包含若干個(gè) Mode,每個(gè) Mode 又包含若干個(gè) Source梆掸、Observer扬卷、Timer。每次調(diào)用 RunLoop 的主函數(shù) __CFRunLoopRun() 時(shí)必須且只能指定一個(gè) Mode,這個(gè) Mode 就被稱為 CurrentMode酸钦;如果需要切換 Mode,只能退出 Loop,在重新指定一個(gè) Mode 進(jìn)入怪得。
CFRunLoopSourceRef 是事件產(chǎn)生的地方。有兩種Source:
- Source0: 僅包含一個(gè)回調(diào)(函數(shù)指針)卑硫,不能主動觸發(fā)事件徒恋;使用時(shí),你需要先調(diào)用
CFRunLoopSourceSignal(source)
欢伏,將這個(gè) Source 標(biāo)記為待處理入挣,然后在調(diào)用 CFRunLoopWakeUp(runloop)來喚醒 RunLoop,讓其處理這個(gè)事件。它負(fù)責(zé) APP 內(nèi)部事件硝拧,由 APP 負(fù)責(zé)管理觸發(fā)径筏,例如 UITouch 事件。 - Source1: 包含一個(gè) mach_port 和一個(gè)回調(diào)(函數(shù)指針)障陶,被用于通過內(nèi)核和其他線程互相發(fā)送消息滋恬。能夠主動喚醒 RunLoop 的線程。它由操作系統(tǒng)內(nèi)核進(jìn)行管理咸这,例如 CFMessagePort 消息夷恍。
CFRunLoopTimeRef 是基于時(shí)間的觸發(fā)器,它和 NSTimer 是 toll-free bridged 的,可以混用酿雪。它包含一個(gè)時(shí)間長度和一個(gè)回調(diào)(函數(shù)指針)遏暴。當(dāng)其加入到 RunLoop 時(shí),RunLoop 會注冊對應(yīng)的時(shí)間點(diǎn)指黎,當(dāng)時(shí)間點(diǎn)到時(shí)朋凉,RunLoop 會被喚醒以執(zhí)行那個(gè)回調(diào)。
CFRunLoopObserverRef 是觀察者醋安,每個(gè) Observer 都包含了一個(gè)回調(diào)(函數(shù)指針)杂彭,當(dāng) RunLoop 的狀態(tài)發(fā)生變化時(shí),觀察者就能通過回調(diào)接受到這個(gè)變化吓揪。我們可以觀測的狀態(tài)有:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
}
上面的 Source/Timer/Observer 被統(tǒng)稱為 mode item亲怠,一個(gè) item 可以被同時(shí)加入多個(gè) mode。但一個(gè) item 被重復(fù)加入同一個(gè) mode 時(shí)是不會有效果的柠辞。如果一個(gè) mode 中一個(gè) item 都沒有团秽,則 RunLoop 會直接退出,不進(jìn)入循環(huán)叭首。
RunLoop 的 Mode
首先习勤,我們先來了解一下 CFRunLoopMode 和 CFRunLoop 的結(jié)構(gòu):
struct __CFRunLoopMode {
CFString _name; // mode name
CFMutableSetRef _sources0;
CFMutableSetRef -sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
···
};
struct __CFRunLoop {
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current RunLoop Mode
CFMutableSetRef _modes;
}
CommonModes
CommonModes: 一個(gè) Mode 可以通過將其 ModeName 添加到 RunLoop 的 "commonModes" 中,從而將自己標(biāo)記為"Common" 屬性焙格。每當(dāng) RunLoop 的內(nèi)容發(fā)生變化時(shí)图毕,RunLoop 都會自動將_commonModeItems 里的 Source/Observer/Timer 同步到具有"Common"標(biāo)記的所有 Mode 里。
應(yīng)用場景:主線程的 RunLoop 里有兩個(gè)預(yù)置的 Mode:KCFRunLoopDefaultMode 和 UITrackingRunLoopMode眷唉。這兩個(gè) Mode 都已經(jīng)被標(biāo)記為"Common"屬性予颤。DefaultMode 是 APP 平時(shí)所處的狀態(tài),TrackingRunLoopMode 是追蹤 ScrollView 滑動時(shí)的狀態(tài)厢破。當(dāng)你創(chuàng)建一個(gè) Timer 并加到 DefaultMode 時(shí)荣瑟, Timer 會得到重復(fù)回調(diào),當(dāng)你滑動 TableView 時(shí)摩泪,Mode 就會切換成為 TrackingRunLoopMode ,這個(gè)時(shí)候 Timer 就不會回調(diào),同時(shí)也不會影響到滑動操作劫谅。
有時(shí)候你需要一個(gè) Timer在兩個(gè) Mode 中都可以得到回調(diào)见坑,方法一就是將這個(gè) Timer 分別加入到這兩個(gè) Mode;方法二就是將 Timer 加入到頂層的 RunLoop 的 "commonModeItems" 中捏检。
Mode 相關(guān)的接口
CFRunLoop 對外暴露管理 Mode 接口:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFString modeName);
CFRunLoopRunInMode(CFStringRef modeName,...);
Mode 對外暴露的管理 item 的接口:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
我們只能通過 modeName 來操作內(nèi)部的 Mode荞驴,如果你傳入一個(gè)新的 modeName 。但是 RunLoop 內(nèi)部不存在時(shí)贯城,RunLoop 會自動創(chuàng)建一個(gè)對應(yīng)的 CFRunLoopModeRef熊楼;對于一個(gè) RunLoop 來說,其內(nèi)部的 mode 只能增加不能刪除能犯。
我們在上面提到了兩種預(yù)置的 Mode鲫骗,當(dāng)我們切換到對應(yīng)的 Mode 時(shí)犬耻,我們只需要傳入對應(yīng)的名稱即可。然而执泰,還存在
KCFRunLoopCommonModes(NSRunloopCommonModes),它是一種組合模式枕磁,在 iOS 默認(rèn)包含了NSDefaultRunLoopMode和UITrackingRunLoopMode(注意:并不是說Runloop會運(yùn)行在 kCFRunLoopCommonModes 這種模式下,而是相當(dāng)于分別注冊了 NSDefaultRunLoopMode 和 UITrackingRunLoopMode术吝。當(dāng)然你也可以通過調(diào)用CFRunLoopAddCommonMode()
方法將自定義Mode放到 kCFRunLoopCommonModes 組合)计济。
注意:
我們常常還會碰到一些系統(tǒng)框架自定義Mode,例如Foundation中NSConnectionReplyMode排苍。還有一些系統(tǒng)私有Mode沦寂,例如:GSEventReceiveRunLoopMode接受系統(tǒng)事件,UIInitializationRunLoopMode App啟動過程中初始化Mode淘衙。
系統(tǒng)默認(rèn)注冊了5個(gè)Mode:
- kCFRunLoopDefaultMode: APP 默認(rèn)的Mode传藏,通常主線程是在這個(gè) Mode 下運(yùn)行。
- UITrackingRunLoopMode: 界面跟蹤 Mode幔翰,用于 ScrollView 追蹤觸摸滑動漩氨,保證界面滑動不受其他 Mode 影響。
- UIInitializationRunLoopMode: 在剛啟動 APP 時(shí)進(jìn)入的第一個(gè) Mode遗增,啟動完之后就不會在使用叫惊。
- GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到。
- kCFRunLoopCommonModes: 占位 Mode做修,沒有實(shí)際作用霍狰。
當(dāng) RunLoop 進(jìn)行回調(diào)時(shí),一般都是通過一個(gè)很長的函數(shù)調(diào)用出去(call out),當(dāng)你在你的代碼中斷點(diǎn)調(diào)試時(shí)饰及,通常能在調(diào)用棧上看到這些函數(shù)蔗坯。
RunLoop 的內(nèi)部邏輯
首先,通過一張圖燎含,來了解一下 RunLoop 的運(yùn)行流程:
內(nèi)部代碼整理如下宾濒;
// 用DefaultMode啟動
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
// 用指定的Mode啟動,允許設(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找到對應(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)用者手動喚醒
__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 總是運(yùn)行在某種特定的 CFRunLoopModeRef 下(每次運(yùn)行__CFRunLoopRun()
函數(shù)時(shí)必須指定 Mode )。它就是一個(gè)帶有一個(gè) do-while 循環(huán)的一個(gè)函數(shù)坠狡。當(dāng)你調(diào)用 CFRunLoopRun() 時(shí)继找,線程就會一直停留在這個(gè)循環(huán)里,只有超時(shí)逃沿、被手動停止或者 item 為空時(shí)婴渡,該函數(shù)才會返回。
RunLoop 的底層實(shí)現(xiàn)
其實(shí)凯亮,對于 RunLoop 而言边臼,最核心的就是保證線程在沒有消息時(shí)休眠從而避免占用系統(tǒng)資源,有消息傳入時(shí)能夠及時(shí)喚醒假消。而這個(gè)機(jī)制完全依靠系統(tǒng)內(nèi)核來完成柠并。
從上一節(jié)的源碼中可以看到,RunLoop 的核心是基于 mach port 的富拗,其進(jìn)入休眠時(shí)調(diào)用的函數(shù)是mach_msg()堂鲤。RunLoop 調(diào)用這個(gè)函數(shù)去接受消息,如果沒有外部發(fā)來的 port消息媒峡,內(nèi)核會一直將線程置于等待狀態(tài)。
RunLoop 的應(yīng)用
定時(shí)器
開頭就提到的Timer Source 作為事件源葵擎,它的上層對應(yīng)的就是 NSTimer(CFRunLoopTimerRef)谅阿。NSTimer 定時(shí)器的觸發(fā)基于 RunLoop 運(yùn)行,使用 NSTimer 之前必須注冊到 RunLoop,但是 RunLoop 為了節(jié)省資源并不會在非常準(zhǔn)確的時(shí)間點(diǎn)調(diào)用定時(shí)器签餐,如果一個(gè)任務(wù)執(zhí)行時(shí)間較長寓涨,那么當(dāng)錯(cuò)過一個(gè)時(shí)間點(diǎn)后只能等待下一個(gè)時(shí)間點(diǎn)執(zhí)行,并不會延后執(zhí)行(NSTimer 提供了一個(gè) tolerance 屬性用于設(shè)置寬容度氯檐,可以通過設(shè)置此屬性來盡可能的使 NSTimer 準(zhǔn)確)戒良。
CADisplayLink 是一個(gè)執(zhí)行頻率(fps)和屏幕刷新相同的定時(shí)器,可以修改preferredFramesPerSecond改變刷新頻率冠摄;它也需要加入到RunLoop才能執(zhí)行糯崎。與NSTimer類似,CADisplayLink同樣是基于CFRunloopTimerRef實(shí)現(xiàn)河泳,底層使用mk_timer(可以比較加入到RunLoop前后RunLoop中timer的變化)沃呢。和NSTimer相比它精度更高(盡管NSTimer也可以修改精度),不過和NStimer類似的是如果遇到大任務(wù)它仍然存在丟幀現(xiàn)象拆挥。通常情況下CADisaplayLink用于構(gòu)建幀動畫薄霜,看起來相對更加流暢,而NSTimer則有更廣泛的用處纸兔。
AutoreleasePool
AutoreleasePool是另一個(gè)與RunLoop相關(guān)討論較多的話題惰瓜。其實(shí)從RunLoop源代碼分析,AutoreleasePool與RunLoop并沒有直接的關(guān)系汉矿,之所以將兩個(gè)話題放到一起討論最主要的原因是因?yàn)樵?APP 啟動后崎坊,蘋果在主線程 RunLoop 里注冊了兩個(gè) Observer 管理和維護(hù) AutorealeasePool,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()
负甸。
第一個(gè) Observer 監(jiān)視的事件是 Entry(即將進(jìn)入 Loop), 其回調(diào)內(nèi)會調(diào)用_objc_autoreleasePoolPush()
創(chuàng)建自動釋放池流强。它的優(yōu)先級最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前呻待。
第二個(gè) Observer 監(jiān)視了兩個(gè)事件: BeforeWaaiting(準(zhǔn)備進(jìn)入休眠)時(shí)調(diào)用_objc_autoreleasePoolPop()
和 _objc_autoreleasePoolPush()
釋放舊的池并創(chuàng)建新的池打月;Exit(即將退出Loop)時(shí)調(diào)用_objc_autoreleasePoolPop()
來釋放自動釋放池。它的優(yōu)先級最低蚕捉,保證其釋放池子發(fā)生在其他所有回調(diào)之后奏篙。
主線程中的其他操作通常均在這個(gè) AutorelsePool 之內(nèi)(main函數(shù)),以盡可能減少內(nèi)存維護(hù)操作迫淹。
事件響應(yīng)
蘋果注冊了一個(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)程。隨后蘋果注冊的那個(gè) Source1 就會觸發(fā)回調(diào)繁仁,并調(diào)用 _UIApplicationHandleEventQueue()
進(jìn)行應(yīng)用內(nèi)部的分發(fā)涉馅。
_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進(jìn)行處理或分發(fā),其中包括識別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給UIWindow等黄虱。通常事件比如 UIButton 點(diǎn)擊稚矿、 TouchesBegin/Move/End/Cancel 事件都是在這個(gè)回調(diào)中完成的。
手勢識別
當(dāng)上面的_UIApplicationHandleEventQueue()
識別了一個(gè)手勢時(shí)捻浦,其首先會調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷晤揣。隨后系統(tǒng)將對應(yīng)的 UIGestureRecognizer 標(biāo)記為待處理。
蘋果注冊了一個(gè) Observer 監(jiān)測 BeforeWaiting (Loop即將進(jìn)入休眠) 事件默勾,這個(gè)Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver()
碉渡,其內(nèi)部會獲取所有剛被標(biāo)記為待處理的 GestureRecognizer,并執(zhí)行 GestureRecognizer 的回調(diào)母剥。
當(dāng)有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時(shí)滞诺,這個(gè)回調(diào)都會進(jìn)行相應(yīng)處理。
界面更新
打印App啟動之后的主線程 RunLoop 可以發(fā)現(xiàn)另外一個(gè)callout為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv
的Observer环疼,這個(gè)監(jiān)聽專門負(fù)責(zé)UI變化后的更新习霹,比如修改了frame、調(diào)整了UI層級(UIView/CALayer)或者手動設(shè)置了setNeedsDisplay/setNeedsLayout之后就會將這些操作提交到全局容器炫隶。
這個(gè) Observers 監(jiān)聽了主線程 RunLoop 的 BeforeWaiting(即將進(jìn)入休眠)和 Exit (即將退出 Loop)狀態(tài)淋叶,一旦進(jìn)入到這兩種狀態(tài)則會遍歷所有的 UI 更新并提交進(jìn)行實(shí)際繪制更新。
該函數(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];
通常情況下這種方式是完美的伪阶,因?yàn)槌讼到y(tǒng)的更新煞檩,還可以利用 setNeedsDisplay 等方法手動觸發(fā)下一次 RunLoop 運(yùn)行的更新。但是如果當(dāng)前正在執(zhí)行大量的邏輯運(yùn)算可能UI的更新就會比較卡栅贴,因此facebook推出了AsyncDisplayKit來解決這個(gè)問題斟湃。有關(guān) AsyncDiskplayKit ,后文會具體講到檐薯。
PerformSelecter
當(dāng)調(diào)用 NSObject 的 performSelecter: afterDelay:
后凝赛,實(shí)際上其內(nèi)部會創(chuàng)建一個(gè) Timer 并添加到當(dāng)前線程的 RunLoop 中。如果當(dāng)前線程沒有 runloop 坛缕,該方法會隨之失效墓猎。
當(dāng)調(diào)用performSelector: onThread:
時(shí),會創(chuàng)建一個(gè) Timer 加到對應(yīng)的線程中去,同樣的赚楚,如果對應(yīng)的線程沒有 RunLoop 該方法也會失效毙沾。
關(guān)于 GCD
在 RunLoop 的源代碼中可以看到 GCD 的相關(guān)東西,但是它倆本質(zhì)是沒有直接關(guān)系宠页。
當(dāng)調(diào)用dispatch_async(dispatch_get_main_queue(), block)
時(shí)搀军,libDispatch 會向主線程的 RunLoop 發(fā)送消息膨俐,RunLoop會被喚醒,并從消息中取得這個(gè) block罩句,并在回調(diào) __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
里執(zhí)行這個(gè) block。但這個(gè)邏輯僅限于 dispatch 到主線程敛摘,dispatch 到其他線程仍然是由 libDispatch 處理的门烂。
關(guān)于網(wǎng)絡(luò)請求
iOS 中,關(guān)于網(wǎng)絡(luò)請求的接口自下而上有如下幾層:
- CFSocket:最底層的接口兄淫,只負(fù)責(zé) socket 通信屯远。
- CFNetwork: 基于 CFSocket 等接口的上層封裝。
- NSURLConnection:基于 CFNetwork 的更高層封裝捕虽,提供面向?qū)ο蟮慕涌?/li>
- NSURLSession 是 iOS7 中新增的接口慨丐,表面上是和 NSURLConnection 并列的,但底層仍然用到了 NSNRLConnection 的部分功能泄私。
NSURLConnection 的工作過程
通常使用 NSURLConnection 時(shí)房揭,你會傳入一個(gè) Delegate,當(dāng)調(diào)用了 [connection start] 后晌端,這個(gè) Delegate 就會不停的收到事件回調(diào)捅暴。實(shí)際上,start 這個(gè)函數(shù)的內(nèi)部會獲取 CurrentRunLoop咧纠,然后在其中的 DefaultMode 添加了4個(gè) Source0(需要手動觸發(fā)的 Source)蓬痒。
- CFHTTPCookieStorage: 用于處理 cookie
- CFMultiplexerSource: 負(fù)責(zé)各種 Delegate 回調(diào),并在回調(diào)中喚醒 Delegate 內(nèi)部的 RunLoop(通常是主線程)來執(zhí)行實(shí)際操作漆羔。
當(dāng)開始網(wǎng)絡(luò)傳輸時(shí)梧奢, NSURLConnection 會創(chuàng)建兩個(gè)新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 線程是處理底層 socket 連接的演痒。NSURLConnectionLoader 這個(gè)線程內(nèi)部會使用 RunLoop 來接收底層 sokcet 的事件亲轨,并通過之前添加的 Source0 通知到上層的 Delegate。
NSURLConnectionLoader 中的 RunLoop 通過一些基于 mach port 的 Source 接收來自底層 CFSocket 的通知嫡霞。當(dāng)收到通知后瓶埋,其會在合適的時(shí)機(jī)向 CFMultiplexerSource 等 Source0 發(fā)送通知,同時(shí)喚醒 Delegate 線程的 RunLoop 來讓其處理這些通知诊沪。CFMultiplexerSource 會在 Delegate 線程的 RunLoop 對 Delegate 執(zhí)行實(shí)際的回調(diào)养筒。
具體實(shí)例舉例
AFNetworking 2.x
AF 2.x 基于NSURLConnection包裝的重要對象,由于iOS9-NSURLConnection已經(jīng)不能使用端姚,AFNetworking在3.x版本中刪除了基于 NSURLConnection API的所有支持晕粪。
因此,我們要研究的就是 AFNetworking 2.x 渐裸。
AFURLConnectionOperation 這個(gè)類是基于 NSURLConnection 構(gòu)建的巫湘。AFNetworking 單獨(dú)創(chuàng)建了一個(gè)線程装悲,并在這個(gè)線程中啟動了一個(gè) RunLoop,在后臺接收 Delegate 回調(diào)。具體代碼如下:
+ (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 啟動前內(nèi)部必須要有至少一個(gè) Timer/Observer/Source尚氛,所以 AFNetworking 在 [runLoop run] 之前先創(chuàng)建了一個(gè)新的 NSMachport 添加進(jìn)去了诀诊。通常情況下,調(diào)用者需要持有這個(gè) NSMachPort(mach_port) 并在外部線程通過這個(gè) port 發(fā)送消息到 loop 內(nèi)阅嘶;此處添加的 port 只是為了讓 RunLoop 不退出属瓣。
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
當(dāng)需要這個(gè)后臺執(zhí)行任務(wù)時(shí), AFNetworking 通過調(diào)用 [NSObject performSelector: onThread:...] 將這個(gè)任務(wù)扔到了后臺線程的 RunLoop 中讯柔。
AsyncDisplayKit
AsyncDisplayKit 是 Facebook 推出的用于保持界面流暢性的框架抡蛙,其原理大致如下:
UI 線程中的任務(wù)通常分為三類:排版、繪制魂迄、UI 對象操作粗截,當(dāng)這些任務(wù)過于繁重的話就會導(dǎo)致界面卡頓。
排版通常包括計(jì)算視圖大小捣炬、計(jì)算文本高度熊昌、重新計(jì)算子視圖的排版等操作。
繪制一般有文本繪制(coreText)遥金、圖片繪制(例如預(yù)先解壓)浴捆、元素繪制(Quartz)等操作。
UI 對象操作通常包括 UIView/CALayer 等 UI 對象的創(chuàng)建稿械、設(shè)置屬性和銷毀选泻。
其中前兩類操作可以通過各種方法放到后臺去執(zhí)行,而最后一項(xiàng)操作只能在主線程完成美莫,并且有時(shí)后面的操作需要依賴前面操作的結(jié)果(TextView 創(chuàng)建時(shí)可能需要提前計(jì)算出文本的大幸趁小)。
AsyncDisplayKit 所做的就是盡量將能放入到后臺的任務(wù)放入后臺厢呵,不能的則盡量推遲(例如視圖的創(chuàng)建窝撵、屬性的調(diào)整)。因此襟铭, AsyncDisplayKit 創(chuàng)建了一個(gè)名為 AsyDisplayNode 的對象碌奉,并在其內(nèi)部封裝了 UIView/CALayer,它具有和 UIView/CALayer 相似的屬性寒砖,例如frame赐劣、backgroundColor 等。所有這些屬性都可以在后臺線程更改哩都,開發(fā)者只可以通過 Node 來操作其內(nèi)部的 UIView/CALayer .魁兼,這些就可以將排版和繪制放入了后臺線程。但是無論怎樣操作漠嵌,但是屬性總需要在某個(gè)時(shí)刻同步到主線程的 UIView/CALayer 去咐汞。
AsyncDisplayKit 仿照 QuartzCore/UIKit 框架的模式盖呼,實(shí)現(xiàn)了一套類似的界面更新的機(jī)制:即在主線程的 RunLoop 中添加一個(gè) Observer,監(jiān)聽了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件化撕,在收到回調(diào)時(shí)几晤,遍歷所有之前放入隊(duì)列的待處理的任務(wù),然后一一執(zhí)行侯谁。