RunLoop 是 iOS 和 OSX 開發(fā)中非趁婧撸基礎(chǔ)的一個概念笛粘,這篇文章將從 CFRunLoop 的源碼入手晰奖,介紹 RunLoop 的概念以及底層實現(xiàn)原理序目。之后會介紹一下在 iOS 中,蘋果是如何利用 RunLoop
實現(xiàn)自動釋放池
岩梳、延遲回調(diào)
囊骤、觸摸事件
、屏幕刷新
等功能的冀值。
RunLoop 的概念
一般來講也物,一個線程一次只能執(zhí)行一個任務(wù),執(zhí)行完成后線程就會退出列疗。如果我們需要一個機制滑蚯,讓線程能隨時處理事件
但并不退出,通常的代碼邏輯是這樣的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
這種模型通常被稱作 Event Loop抵栈。 Event Loop
在很多系統(tǒng)和框架里都有實現(xiàn)告材,比如 Node.js 的事件處理,比如 Windows 程序的消息循環(huán)古劲,再比如 OSX/iOS 里的 RunLoop
斥赋。實現(xiàn)這種模型的關(guān)鍵點在于:如何管理事件/消息
,如何讓線程在沒有處理消息時休眠以避免資源占用绢慢、在有消息到來時立刻被喚醒灿渴。
所以,RunLoop 實際上就是一個對象胰舆,這個對象管理了其需要處理的事件和消息骚露,并提供了一個入口函數(shù)來執(zhí)行上面 Event Loop 的邏輯。線程
執(zhí)行了這個函數(shù)后缚窿,就會一直處于
這個函數(shù)內(nèi)部 “接受消息->等待->處理” 的循環(huán)
中棘幸,直到這個循環(huán)結(jié)束
(比如傳入 quit 的消息),函數(shù)返回倦零。
OSX/iOS 系統(tǒng)中误续,提供了兩個這樣的對象:NSRunLoop
和 CFRunLoopRef
。
CFRunLoopRef
是在 CoreFoundation 框架內(nèi)的扫茅,它提供了純 C 函數(shù)的 API蹋嵌,所有這些 API 都是線程安全的
。
NSRunLoop
是基于 CFRunLoopRef 的封裝葫隙,提供了面向?qū)ο蟮?API栽烂,但是這些 API 不是線程安全的
。
CFRunLoopRef 的代碼是開源的,你可以在這里 http://opensource.apple.com/tarballs/CF/ 下載到整個 CoreFoundation 的源碼來查看腺办。
(Update: Swift 開源后焰手,蘋果又維護了一個跨平臺的 CoreFoundation 版本:https://github.com/apple/swift-corelibs-foundation/,這個版本的源碼可能和現(xiàn)有 iOS 系統(tǒng)中的實現(xiàn)略不一樣怀喉,但更容易編譯书妻,而且已經(jīng)適配了 Linux/Windows。)
RunLoop 與線程的關(guān)系
首先躬拢,iOS 開發(fā)中能遇到兩個線程對象: pthread_t
和 NSThread
躲履。過去蘋果有份文檔標(biāo)明了 NSThread 只是 pthread_t 的封裝,但那份文檔已經(jīng)失效了估灿,現(xiàn)在它們也有可能都是直接包裝自最底層的 mach thread崇呵。蘋果并沒有提供這兩個對象相互轉(zhuǎn)換的接口,但不管怎么樣馅袁,可以肯定的是 pthread_t 和 NSThread 是一一對應(yīng)的
。比如荒辕,你可以通過 pthread_main_thread_np() 或 [NSThread mainThread] 來獲取主線程汗销;也可以通過 pthread_self() 或 [NSThread currentThread] 來獲取當(dāng)前線程。CFRunLoop 是基于 pthread 來管理的
抵窒。
蘋果不允許直接創(chuàng)建 RunLoop弛针,它只提供了兩個自動獲取的函數(shù):CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
。 這兩個函數(shù)內(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),當(dāng)線程銷毀時哪亿,順便也銷毀其對應(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());
}
從上面的代碼可以看出,線程和 RunLoop 之間是一一對應(yīng)的
蝇棉,其關(guān)系是保存在一個全局的 Dictionary 里讨阻。線程剛創(chuàng)建時并沒有 RunLoop,如果你不主動獲取篡殷,那它一直都不會有钝吮。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時,RunLoop 的銷毀是發(fā)生在線程結(jié)束時
。你只能在一個線程的內(nèi)部獲取其 RunLoop(主線程除外)
搀绣。
RunLoop 對外的接口
在 CoreFoundation 里面關(guān)于 RunLoop 有5個類:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
其中 CFRunLoopModeRef 類并沒有對外暴露飞袋,只是通過 CFRunLoopRef 的接口進行了封裝。他們的關(guān)系如下:
一個 RunLoop
包含若干個 Mode
链患,每個 Mode 又包含若干個 Source/Timer/Observer
巧鸭。每次調(diào)用 RunLoop 的主函數(shù)
時,只能指定其中一個 Mode
麻捻,這個Mode被稱作 CurrentMode
纲仍。如果需要切換 Mode,只能退出 Loop贸毕,再重新指定一個 Mode 進入
郑叠。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響
明棍。
CFRunLoopSourceRef
是事件產(chǎn)生的地方
乡革。Source有兩個版本:Source0
和 Source1
。
? Source0 只包含了一個回調(diào)(函數(shù)指針)摊腋,它并不能主動觸發(fā)事件
沸版。使用時,你需要先調(diào)用 CFRunLoopSourceSignal(source)
兴蒸,將這個 Source 標(biāo)記為待處理视粮,然后手動調(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ù)指針)。當(dāng)其加入到 RunLoop 時堕虹,RunLoop會注冊對應(yīng)的時間點卧晓,當(dāng)時間點到時,RunLoop會被喚醒
以執(zhí)行那個回調(diào)赴捞。
CFRunLoopObserverRef
是觀察者
逼裆,每個 Observer 都包含了一個回調(diào)(函數(shù)指針),當(dāng) 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
};
上面的 Source/Timer/Observer
被統(tǒng)稱為 mode item
,一個 item 可以被同時加入多個 mode
桐愉。但一個 item 被重復(fù)加入同一個 mode 時是不會有效果的
财破。如果一個mode 中一個 item 都沒有,則 RunLoop 會直接退出
从诲,不進入循環(huán)左痢。
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
...
};
這里有個概念叫“CommonModes”
:一個 Mode 可以將自己標(biāo)記為”Common”屬性(通過將其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每當(dāng) RunLoop 的內(nèi)容發(fā)生變化時系洛,RunLoop 都會自動將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 標(biāo)記的所有Mode里俊性。
應(yīng)用場景舉例:主線程的 RunLoop
里有兩個預(yù)置的 Mode:kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
。這兩個 Mode 都已經(jīng)被標(biāo)記為”Common”屬性
描扯。DefaultMode 是 App 平時所處的狀態(tài)
定页,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態(tài)。當(dāng)你創(chuàng)建一個 Timer 并加到 DefaultMode
時绽诚,Timer 會得到重復(fù)回調(diào)
典徊,但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode
憔购,這時 Timer 就不會被回調(diào)
(造成 NSTimer 卡頓的原因
)宫峦,并且也不會影響到滑動操作。
有時你需要一個 Timer
玫鸟,在兩個 Mode 中都能得到回調(diào)
,一種辦法就是將這個 Timer 分別加入這兩個 Mode
犀勒。還有一種方式屎飘,就是將 Timer 加入到頂層的 RunLoop 的 “commonModeItems” 中
〖址眩”commonModeItems” 被 RunLoop 自動更新到所有具有”Common”屬性的 Mode 里去钦购。
CFRunLoop對外暴露的管理 Mode 接口只有下面2個:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
Mode 暴露的管理 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);
你只能通過 mode name 來操作內(nèi)部的 mode
,當(dāng)你傳入一個新的 mode name 但 RunLoop 內(nèi)部沒有對應(yīng) mode 時褂萧,RunLoop會自動幫你創(chuàng)建對應(yīng)的 CFRunLoopModeRef
押桃。對于一個 RunLoop 來說,其內(nèi)部的 mode 只能增加不能刪除导犹。
蘋果公開提供的 Mode 有兩個:kCFRunLoopDefaultMode (NSDefaultRunLoopMode)
和UITrackingRunLoopMode
唱凯,你可以用這兩個 Mode Name 來操作其對應(yīng)的 Mode。
同時蘋果還提供了一個操作 Common 標(biāo)記的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes)
谎痢,你可以用這個字符串來操作 Common Items磕昼,或標(biāo)記一個 Mode 為 “Common”。使用時注意區(qū)分這個字符串和其他 mode name节猿。
RunLoop 的內(nèi)部邏輯
根據(jù)蘋果在文檔里的說明票从,RunLoop 內(nèi)部的邏輯大致如下:
其內(nèi)部代碼整理如下 (太長了不想看可以直接跳過去,后面會有說明):
/// 用DefaultMode啟動
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode啟動,允許設(shè)置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的實現(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 即將進入 loop浸间。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 內(nèi)部函數(shù),進入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)煤墙,直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息梅惯。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息仿野。線程將進入休眠, 直到被下面某一個事件喚醒铣减。
/// ? 一個基于 port 的Source 的事件。
/// ? 一個 Timer 到時間了
/// ? RunLoop 自身的超時時間到了
/// ? 被其他什么調(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 如果一個 Timer 到時間了球涛,觸發(fā)這個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 如果一個 Source1 (基于port) 發(fā)出事件了捺典,處理這個事件
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) {
/// 進入loop時參數(shù)說處理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出傳入?yún)?shù)標(biāo)記的超時時間了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部調(diào)用者強制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一個都沒有了
retVal = kCFRunLoopRunFinished;
}
/// 如果沒超時从祝,mode里沒空襟己,loop也沒被停止,那繼續(xù)loop牍陌。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即將退出擎浴。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
可以看到,實際上 RunLoop
就是這樣一個函數(shù)毒涧,其內(nèi)部是一個 do-while 循環(huán)
贮预。當(dāng)你調(diào)用 CFRunLoopRun() 時
,線程就會一直停留在這個循環(huán)里
契讲;直到超時或被手動停止仿吞,該函數(shù)才會返回。
RunLoop 的底層實現(xiàn)
從上面代碼可以看到怀泊,RunLoop 的核心是基于 mach port 的茫藏,其進入休眠時調(diào)用的函數(shù)是 mach_msg()。為了解釋這個邏輯霹琼,下面稍微介紹一下 OSX/iOS 的系統(tǒng)架構(gòu)务傲。
蘋果官方將整個系統(tǒng)大致劃分為上述4個層次:
應(yīng)用層包括用戶能接觸到的圖形應(yīng)用凉当,例如 Spotlight、Aqua售葡、SpringBoard 等看杭。
應(yīng)用框架層即開發(fā)人員接觸到的 Cocoa 等框架。
核心框架層包括各種核心框架挟伙、OpenGL 等內(nèi)容楼雹。
Darwin 即操作系統(tǒng)的核心,包括系統(tǒng)內(nèi)核尖阔、驅(qū)動贮缅、Shell 等內(nèi)容,這一層是開源的介却,其所有源碼都可以在 opensource.apple.com 里找到谴供。
我們在深入看一下 Darwin 這個核心的架構(gòu):
其中,在硬件層上面的三個組成部分:Mach齿坷、BSD桂肌、IOKit (還包括一些上面沒標(biāo)注的內(nèi)容),共同組成了 XNU 內(nèi)核永淌。
XNU
內(nèi)核的內(nèi)環(huán)
被稱作 Mach
崎场,其作為一個微內(nèi)核
,僅提供了諸如處理器調(diào)度遂蛀、IPC (進程間通信)等非常少量的基礎(chǔ)服務(wù)谭跨。
BSD
層可以看作圍繞 Mach 層
的一個外環(huán)
,其提供了諸如進程管理李滴、文件系統(tǒng)和網(wǎng)絡(luò)等功能饺蚊。
IOKit
層是為設(shè)備驅(qū)動
提供了一個面向?qū)ο?C++)的一個框架。
Mach 本身提供的 API 非常有限悬嗓,而且蘋果也不鼓勵使用 Mach 的 API,但是這些API非吃7唬基礎(chǔ)包竹,如果沒有這些API的話,其他任何工作都無法實施籍凝。在 Mach 中
周瞎,所有的東西都是通過自己的對象實現(xiàn)的,進程饵蒂、線程和虛擬內(nèi)存都被稱為”對象”
声诸。和其他架構(gòu)不同, Mach 的對象間不能直接調(diào)用退盯,只能通過消息傳遞的方式實現(xiàn)對象間的通信
彼乌⌒嚎希”消息”是 Mach 中最基礎(chǔ)的概念,消息在兩個端口 (port) 之間傳遞慰照,這就是 Mach 的 IPC (進程間通信) 的核心灶挟。
Mach 的消息定義是在 <mach/message.h> 頭文件的,很簡單:
typedef struct {
mach_msg_header_t header;
mach_msg_body_t body;
} mach_msg_base_t;
typedef struct {
mach_msg_bits_t msgh_bits;
mach_msg_size_t msgh_size;
mach_port_t msgh_remote_port;
mach_port_t msgh_local_port;
mach_port_name_t msgh_voucher_port;
mach_msg_id_t msgh_id;
} mach_msg_header_t;
一條 Mach 消息實際上就是一個二進制數(shù)據(jù)包 (BLOB)毒租,其頭部定義了當(dāng)前端口 local_port
和目標(biāo)端口 remote_port
稚铣,
發(fā)送和接受消息是通過同一個 API 進行的
,其 option 標(biāo)記了消息傳遞的方向:
mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t rcv_size,
mach_port_name_t rcv_name,
mach_msg_timeout_t timeout,
mach_port_name_t notify);
為了實現(xiàn)消息的發(fā)送和接收墅垮,mach_msg() 函數(shù)
實際上是調(diào)用了一個 Mach 陷阱 (trap)
惕医,即函數(shù)mach_msg_trap()
,陷阱這個概念在 Mach 中等同于系統(tǒng)調(diào)用
算色。當(dāng)你在用戶態(tài)調(diào)用 mach_msg_trap() 時會觸發(fā)陷阱機制抬伺,切換到內(nèi)核態(tài);內(nèi)核態(tài)中內(nèi)核實現(xiàn)的 mach_msg() 函數(shù)會完成實際的工作剃允,如下圖:
這些概念可以參考維基百科: System_call沛简、Trap_(computing)。
RunLoop 的核心就是一個 mach_msg()
(見上面代碼的第7步)斥废,RunLoop 調(diào)用這個函數(shù)去接收消息椒楣,如果沒有別人發(fā)送 port 消息過來,內(nèi)核會將線程置于等待狀態(tài)牡肉。例如你在模擬器里跑起一個 iOS 的 App捧灰,然后在 App 靜止時點擊暫停,你會看到主線程調(diào)用棧是停留在 mach_msg_trap() 這個地方统锤。
關(guān)于具體的如何利用 mach port 發(fā)送信息毛俏,可以看看 NSHipster 這一篇文章,或者這里的中文翻譯 饲窿。
關(guān)于Mach的歷史可以看看這篇很有趣的文章:Mac OS X 背后的故事(三)Mach 之父 Avie Tevanian煌寇。
蘋果用 RunLoop 實現(xiàn)的功能
首先我們可以看一下 App 啟動后 RunLoop 的狀態(tài):
CFRunLoop {
current mode = kCFRunLoopDefaultMode
common modes = {
UITrackingRunLoopMode
kCFRunLoopDefaultMode
}
common mode items = {
// source0 (manual)
CFRunLoopSource {order =-1, {
callout = _UIApplicationHandleEventQueue}}
CFRunLoopSource {order =-1, {
callout = PurpleEventSignalCallback }}
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}
// source1 (mach port)
CFRunLoopSource {order = 0, {port = 17923}}
CFRunLoopSource {order = 0, {port = 12039}}
CFRunLoopSource {order = 0, {port = 16647}}
CFRunLoopSource {order =-1, {
callout = PurpleEventCallback}}
CFRunLoopSource {order = 0, {port = 2407,
callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}}
CFRunLoopSource {order = 0, {port = 1c03,
callout = __IOHIDEventSystemClientAvailabilityCallback}}
CFRunLoopSource {order = 0, {port = 1b03,
callout = __IOHIDEventSystemClientQueueCallback}}
CFRunLoopSource {order = 1, {port = 1903,
callout = __IOMIGMachPortPortCallback}}
// Ovserver
CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry
callout = _wrapRunLoopWithAutoreleasePoolHandler}
CFRunLoopObserver {order = 0, activities = 0x20, // BeforeWaiting
callout = _UIGestureRecognizerUpdateObserver}
CFRunLoopObserver {order = 1999000, activities = 0xa0, // BeforeWaiting | Exit
callout = _afterCACommitHandler}
CFRunLoopObserver {order = 2000000, activities = 0xa0, // BeforeWaiting | Exit
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit
callout = _wrapRunLoopWithAutoreleasePoolHandler}
// Timer
CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0,
next fire date = 453098071 (-4421.76019 @ 96223387169499),
callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)}
},
modes = {
CFRunLoopMode {
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },
},
CFRunLoopMode {
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },
},
CFRunLoopMode {
sources0 = {
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}
},
sources1 = (null),
observers = {
CFRunLoopObserver >{activities = 0xa0, order = 2000000,
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
)},
timers = (null),
},
CFRunLoopMode {
sources0 = {
CFRunLoopSource {order = -1, {
callout = PurpleEventSignalCallback}}
},
sources1 = {
CFRunLoopSource {order = -1, {
callout = PurpleEventCallback}}
},
observers = (null),
timers = (null),
},
CFRunLoopMode {
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null),
}
}
}
可以看到,系統(tǒng)默認注冊了5個Mode:
1. kCFRunLoopDefaultMode: App的默認 Mode逾雄,通常主線程是在這個 Mode 下運行的阀溶。
2. UITrackingRunLoopMode: 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動鸦泳,保證界面滑動時不受其他 Mode 影響银锻。
3. UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用做鹰。
4. GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode击纬,通常用不到。
5. kCFRunLoopCommonModes: 這是一個占位的 Mode钾麸,沒有實際作用更振。
你可以在這里看到更多的蘋果內(nèi)部的 Mode炕桨,但那些 Mode 在開發(fā)中就很難遇到了
當(dāng) RunLoop 進行回調(diào)時,一般都是通過一個很長的函數(shù)調(diào)用出去 (call out), 當(dāng)你在你的代碼中下斷點調(diào)試時殃饿,通常能在調(diào)用棧上看到這些函數(shù)谋作。下面是這幾個函數(shù)的整理版本,如果你在調(diào)用棧中看到這些長函數(shù)名乎芳,在這里查找一下就能定位到具體的調(diào)用地點了:
{
/// 1. 通知Observers遵蚜,即將進入RunLoop
/// 此處有Observer會創(chuàng)建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即將觸發(fā) Timer 回調(diào)。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即將觸發(fā) Source (非基于port的,Source0) 回調(diào)奈惑。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 觸發(fā) Source0 (非基于port的) 回調(diào)吭净。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即將進入休眠
/// 此處有Observer釋放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers肴甸,線程被喚醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer喚醒的寂殉,回調(diào)Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch喚醒的,執(zhí)行所有調(diào)用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件喚醒了原在,處理這個事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers友扰,即將退出RunLoop
/// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
AutoreleasePool
App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer
庶柿,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()村怪。
第一個 Observe
r 監(jiān)視的事件是 Entry
(即將進入Loop)褥伴,其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush()
創(chuàng)建自動釋放池
阿纤。其 order 是-2147483647,優(yōu)先級最高窝剖,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前审残。
第二個 Observer
監(jiān)視了兩個事件: BeforeWaiting(準(zhǔn)備進入休眠)
時調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池梭域;Exit(即將退出Loop)
時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647搅轿,優(yōu)先級最低病涨,保證其釋放池子發(fā)生在其他所有回調(diào)之后。
在主線程執(zhí)行的代碼璧坟,通常是寫在諸如事件回調(diào)没宾、Timer回調(diào)內(nèi)的。這些回調(diào)會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著沸柔,所以不會出現(xiàn)內(nèi)存泄漏,開發(fā)者也不必顯示創(chuàng)建 Pool 了铲敛。
事件響應(yīng)
蘋果注冊了一個 Source (基于 mach port 的) 用來接收系統(tǒng)事件褐澎,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()。
說明:原文為Source1伐蒋,實際用Xcode打印出來的調(diào)試日志為CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION工三,即Source0迁酸。所以這里用Source來代替事件源。
當(dāng)一個硬件事件(觸摸/鎖屏/搖晃等
)發(fā)生后俭正,首先由 IOKit.framework
生成一個IOHIDEvent 事件
并由 SpringBoard
接收奸鬓。這個過程的詳細情況可以參考這里。SpringBoard
只接收按鍵(鎖屏/靜音等)掸读,觸摸串远,加速,接近傳感器等
幾種 Event
儿惫,隨后用 mach port
轉(zhuǎn)發(fā)給需要的App進程澡罚。隨后蘋果注冊的那個 Source 就會觸發(fā)回調(diào),并調(diào)用 _UIApplicationHandleEventQueue()
進行應(yīng)用內(nèi)部的分發(fā)肾请。
_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進行處理或分發(fā)留搔,其中包括識別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等。通常事件比如 UIButton 點擊铛铁、touchesBegin/Move/End/Cancel 事件都是在這個回調(diào)中完成的隔显。
手勢識別
當(dāng)上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷饵逐。隨后系統(tǒng)將對應(yīng)的 UIGestureRecognizer 標(biāo)記為待處理括眠。
蘋果注冊了一個 Observer 監(jiān)測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver()梳毙,其內(nèi)部會獲取所有剛被標(biāo)記為待處理的 GestureRecognizer哺窄,并執(zhí)行GestureRecognizer的回調(diào)。
當(dāng)有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時账锹,這個回調(diào)都會進行相應(yīng)處理萌业。
界面更新
當(dāng)在操作 UI 時,比如改變了 Frame奸柬、更新了 UIView/CALayer 的層次時生年,或者手動調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標(biāo)記為待處理廓奕,并被提交到一個全局的容器去抱婉。
蘋果注冊了一個 Observer 監(jiān)聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調(diào)去執(zhí)行一個很長的函數(shù):
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()桌粉。這個函數(shù)里會遍歷所有待處理的 UIView/CAlayer 以執(zhí)行實際的繪制和調(diào)整蒸绩,并更新 UI 界面。
這個函數(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];
定時器
NSTimer
其實就是 CFRunLoopTimerRef
铃肯,他們之間是 toll-free bridged 的(可以參考NSTimer官方文檔)患亿。一個 NSTimer 注冊到 RunLoop 后,RunLoop 會為其重復(fù)的時間點注冊好事件押逼。例如 10:00, 10:10, 10:20 這幾個時間點步藕。RunLoop為了節(jié)省資源惦界,并不會在非常準(zhǔn)確的時間點回調(diào)這個Timer。Timer 有個屬性叫做 Tolerance (寬容度)
咙冗,標(biāo)示了當(dāng)時間點到后沾歪,容許有多少最大誤差
。
如果某個時間點被錯過了
雾消,例如執(zhí)行了一個很長的任務(wù)灾搏,則那個時間點的回調(diào)也會跳過去,不會延后執(zhí)行
仪或。就比如等公交确镊,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了范删。
CADisplayLink 是一個和屏幕刷新率一致的定時器
(但實際實現(xiàn)原理更復(fù)雜蕾域,和 NSTimer 并不一樣
,其內(nèi)部實際是操作了一個 Source)到旦。如果在兩次屏幕刷新之間執(zhí)行了一個長任務(wù)旨巷,那其中就會有一幀被跳過去(和 NSTimer 相似),造成界面卡頓的感覺
添忘。在快速滑動TableView時采呐,即使一幀的卡頓也會讓用戶有所察覺。Facebook 開源的 AsyncDisplayLink 就是為了解決界面卡頓的問題搁骑,其內(nèi)部也用到了 RunLoop
斧吐,這個稍后我會再單獨寫一頁博客來分析。
PerformSelecter
當(dāng)調(diào)用 NSObject 的 performSelecter:afterDelay:
后仲器,實際上其內(nèi)部會創(chuàng)建一個 Timer
并添加到當(dāng)前線程的 RunLoop 中
煤率。所以如果當(dāng)前線程沒有 RunLoop,則這個方法會失效乏冀。
當(dāng)調(diào)用 performSelector:onThread: 時蝶糯,實際上其會創(chuàng)建一個 Timer 加到對應(yīng)的線程去,同樣的辆沦,如果對應(yīng)線程沒有 RunLoop 該方法也會失效昼捍。
關(guān)于GCD
實際上 RunLoop 底層也會用到 GCD 的東西,比如 RunLoop 是用 dispatch_source_t 實現(xiàn)的 Timer(評論中有人提醒肢扯,NSTimer 是用了 XNU 內(nèi)核的 mk_timer妒茬,我也仔細調(diào)試了一下,發(fā)現(xiàn) NSTimer 確實是由 mk_timer 驅(qū)動蔚晨,而非 GCD 驅(qū)動的)郊闯。但同時 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。
當(dāng)調(diào)用 dispatch_async(dispatch_get_main_queue(), block) 時团赁,libDispatch 會向主線程的 RunLoop 發(fā)送消息,RunLoop會被喚醒谨履,并從消息中取得這個 block欢摄,并在回調(diào) CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里執(zhí)行這個 block。但這個邏輯僅限于 dispatch 到主線程笋粟,dispatch 到其他線程仍然是由 libDispatch 處理的怀挠。
關(guān)于網(wǎng)絡(luò)請求
iOS 中,關(guān)于網(wǎng)絡(luò)請求的接口自下至上有如下幾層:
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire
? CFSocket
是最底層的接口害捕,只負責(zé) socket 通信
绿淋。
? CFNetwork
是基于 CFSocket 等接口的上層封裝,ASIHttpRequest 工作于這一層
尝盼。
? NSURLConnection
是基于 CFNetwork 的更高層的封裝吞滞,提供面向?qū)ο蟮慕涌冢?code>AFNetworking 工作于這一層。
? NSURLSession 是 iOS7 中新增的接口
盾沫,表面上是和 NSURLConnection 并列的裁赠,但底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程),AFNetworking2 和 Alamofire 工作于這一層
赴精。
下面主要介紹下 NSURLConnection 的工作過程佩捞。
通常使用 NSURLConnection 時,你會傳入一個 Delegate蕾哟,當(dāng)調(diào)用了 [connection start] 后一忱,這個 Delegate 就會不停收到事件回調(diào)。實際上谭确,start 這個函數(shù)的內(nèi)部會會獲取 CurrentRunLoop
帘营,然后在其中的 DefaultMode 添加了4個 Source0 (即需要手動觸發(fā)的Source)。CFMultiplexerSource
是負責(zé)各種 Delegate 回調(diào)的
琼富,CFHTTPCookieStorage
是處理各種 Cookie
的仪吧。
當(dāng)開始網(wǎng)絡(luò)傳輸時,我們可以看到 NSURLConnection 創(chuàng)建了兩個新線程:com.apple.NSURLConnectionLoader
和 com.apple.CFSocket.private
鞠眉。其中 CFSocket 線程是處理底層 socket 連接的薯鼠。NSURLConnectionLoader 這個線程內(nèi)部會使用 RunLoop 來接收底層 socket 的事件,并通過之前添加的 Source0 通知到上層的 Delegate
械蹋。
NSURLConnectionLoader 中的 RunLoop 通過一些基于 mach port
的 Source 接收來自底層 CFSocket 的通知出皇。當(dāng)收到通知后,其會在合適的時機向 CFMultiplexerSource 等 Source0 發(fā)送通知哗戈,同時喚醒 Delegate 線程的 RunLoop 來讓其處理這些通知郊艘。CFMultiplexerSource 會在 Delegate 線程的 RunLoop 對 Delegate 執(zhí)行實際的回調(diào)。
RunLoop 的實際應(yīng)用舉例
AFNetworking
AFURLConnectionOperation 這個類是基于 NSURLConnection 構(gòu)建的,其希望能在后臺線程接收 Delegate 回調(diào)纱注。為此 AFNetworking 單獨創(chuàng)建了一個線程畏浆,并在這個線程中啟動了一個 RunLoop:
+ (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)部必須要有至少一個 Timer/Observer/Source
,所以 AFNetworking 在 [runLoop run] 之前先創(chuàng)建了一個新的 NSMachPort
添加進去了狞贱。通常情況下刻获,調(diào)用者需要持有這個 NSMachPort (mach_port) 并在外部線程通過這個 port 發(fā)送消息到 loop 內(nèi);但此處添加 port 只是為了讓 RunLoop 不至于退出
瞎嬉,并沒有用于實際的發(fā)送消息蝎毡。
- (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)需要這個后臺線程執(zhí)行任務(wù)時,AFNetworking 通過調(diào)用 [NSObject performSelector:onThread:..] 將這個任務(wù)扔到了后臺線程的 RunLoop 中氧枣。
AsyncDisplayKit
AsyncDisplayKit 是 Facebook 推出的用于保持界面流暢性的框架
沐兵,其原理大致如下:
UI 線程中一旦出現(xiàn)繁重的任務(wù)就會導(dǎo)致界面卡頓,這類任務(wù)通常分為3類:排版
便监,繪制
扎谎,UI對象操作
。
排版
通常包括計算視圖大小茬贵、計算文本高度簿透、重新計算子式圖的排版等操作
。
繪制
一般有文本繪制 (例如 CoreText)解藻、圖片繪制 (例如預(yù)先解壓)老充、元素繪制 (Quartz)等操作
。
UI對象操作
通常包括 UIView/CALayer 等 UI 對象的創(chuàng)建螟左、設(shè)置屬性和銷毀
啡浊。
其中前兩類操作
可以通過各種方法扔到后臺線程執(zhí)行
,而最后一類操作只能在主線程完成
胶背,并且有時后面的操作需要依賴前面操作的結(jié)果 (例如TextView創(chuàng)建時可能需要提前計算出文本的大邢锵)。ASDK 所做的钳吟,就是盡量將能放入后臺的任務(wù)放入后臺廷粒,不能的則盡量推遲 (例如視圖的創(chuàng)建、屬性的調(diào)整)
红且。
為此坝茎,ASDK 創(chuàng)建了一個名為 ASDisplayNode 的對象,并在內(nèi)部封裝了 UIView/CALayer暇番,它具有和 UIView/CALayer 相似的屬性嗤放,例如 frame、backgroundColor等壁酬。所有這些屬性都可以在后臺線程更改次酌,開發(fā)者可以只通過 Node 來操作其內(nèi)部的 UIView/CALayer恨课,這樣就可以將排版和繪制放入了后臺線程。但是無論怎么操作岳服,這些屬性總需要在某個時刻同步到主線程的 UIView/CALayer 去剂公。
ASDK
仿照 QuartzCore/UIKit 框架的模式,實現(xiàn)了一套類似的界面更新的機制:即在主線程的 RunLoop 中添加一個 Observer
吊宋,監(jiān)聽
了 kCFRunLoopBeforeWaiting
和 kCFRunLoopExit
事件诬留,在收到回調(diào)時,遍歷所有之前放入隊列的待處理的任務(wù)贫母,然后一一執(zhí)行。
具體的代碼可以看這里:_ASAsyncTransactionGroup盒刚。
原文連接:https://blog.ibireme.com/2015/05/18/runloop/
原文代碼可能過時腺劣,部分解析可能存在問題。所以對可能存在問題的內(nèi)容因块,本文進行了說明橘原,避免產(chǎn)生誤導(dǎo)。