Runloop從語法上分析
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
UIApplicationMain內(nèi)部默認(rèn)開啟了主線程的RunLoop,并執(zhí)行了一段無限循環(huán)的代碼(不是簡單的for循環(huán)或while循環(huán)),UIApplicationMain函數(shù)一直沒有返回,而是不斷地接收處理消息以及等待休眠,所以運(yùn)行程序之后會保持持續(xù)運(yùn)行狀態(tài)。
NSRunLoop(Foundation)是CFRunLoop(CoreFoundation)的封裝,提供了面向?qū)ο蟮腁PI
RunLoop 相關(guān)的主要涉及五個類:
CFRunLoop:RunLoop對象
CFRunLoopMode:運(yùn)行模式
CFRunLoopSource:輸入源/事件源
CFRunLoopTimer:定時源
CFRunLoopObserver:觀察者
1、CFRunLoop
由pthread(線程對象,說明RunLoop和線程是一一對應(yīng)的)、currentMode(當(dāng)前所處的運(yùn)行模式)、modes(多個運(yùn)行模式的集合)、commonModes(模式名稱字符串集合)、commonModelItems(Observer,Timer,Source集合)構(gòu)成
2、CFRunLoopMode
由name、source0、source1、observers、timers構(gòu)成
3、CFRunLoopSource
分為source0和source1兩種
source0:
即非基于port的,一般是APP內(nèi)部的事件,只包含了一個回調(diào)(函數(shù)指針)。需要手動喚醒線程,將當(dāng)前線程從內(nèi)核態(tài)切換到用戶態(tài),它并不能主動觸發(fā)事件。需要先調(diào)用 (source),將這個 Source 標(biāo)記為待處理,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
source1:
基于port的,包含一個 mach_port 和一個回調(diào)(函數(shù)指針),可監(jiān)聽系統(tǒng)端口和通過內(nèi)核和其他線程發(fā)送的消息,能主動喚醒RunLoop,接收分發(fā)系統(tǒng)事件。具備喚醒線程的能力。
4、CFRunLoopTimer
基于時間的觸發(fā)器,基本上說的就是NSTimer。在預(yù)設(shè)的時間點(diǎn)喚醒RunLoop執(zhí)行回調(diào)。因?yàn)樗腔赗unLoop的,因此它不是實(shí)時的(就是NSTimer 是不準(zhǔn)確的。 因?yàn)镽unLoop只負(fù)責(zé)分發(fā)源的消息。如果線程當(dāng)前正在處理繁重的任務(wù),就有可能導(dǎo)致Timer本次延時,或者少執(zhí)行一次)。
5、CFRunLoopObserver
監(jiān)聽以下時間點(diǎn):CFRunLoopActivity
kCFRunLoopEntry
RunLoop準(zhǔn)備啟動
kCFRunLoopBeforeTimers
RunLoop將要處理一些Timer相關(guān)事件
kCFRunLoopBeforeSources
RunLoop將要處理一些Source事件
kCFRunLoopBeforeWaiting
RunLoop將要進(jìn)行休眠狀態(tài),即將由用戶態(tài)切換到內(nèi)核態(tài)
kCFRunLoopAfterWaiting
RunLoop被喚醒,即從內(nèi)核態(tài)切換到用戶態(tài)后
kCFRunLoopExit
RunLoop退出
kCFRunLoopAllActivities
監(jiān)聽所有狀態(tài)
6、各數(shù)據(jù)結(jié)構(gòu)之間的聯(lián)系
線程和RunLoop一一對應(yīng), RunLoop和Mode是一對多的,Mode和source、timer、observer也是一對多的
三、RunLoop的Mode
關(guān)于Mode首先要知道一個RunLoop 對象中可能包含多個Mode,且每次調(diào)用 RunLoop 的主函數(shù)時,只能指定其中一個 Mode(CurrentMode)。切換 Mode,需要重新指定一個 Mode 。主要是為了分隔開不同的 Source、Timer、Observer,讓它們之間互不影響。
當(dāng)RunLoop運(yùn)行在Mode1上時,是無法接受處理Mode2或Mode3上的Source、Timer、Observer事件的,
總共是有五種CFRunLoopMode:
kCFRunLoopDefaultMode:默認(rèn)模式,主線程是在這個運(yùn)行模式下運(yùn)行
UITrackingRunLoopMode:跟蹤用戶交互事件(用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他Mode影響)
UIInitializationRunLoopMode:在剛啟動App時第進(jìn)入的第一個 Mode,啟動完成后就不再使用
GSEventReceiveRunLoopMode:接受系統(tǒng)內(nèi)部事件,通常用不到
kCFRunLoopCommonModes:偽模式,不是一種真正的運(yùn)行模式,是同步Source/Timer/Observer到多個Mode中的一種解決方案
四、RunLoop的實(shí)現(xiàn)機(jī)制
對于RunLoop而言最核心的事情就是保證線程在沒有消息的時候休眠,在有消息時喚醒,以提高程序性能。RunLoop這個機(jī)制是依靠系統(tǒng)內(nèi)核來完成的(蘋果操作系統(tǒng)核心組件Darwin中的Mach)。
RunLoop通過mach_msg()
函數(shù)接收、發(fā)送消息。它的本質(zhì)是調(diào)用函數(shù)mach_msg_trap()
,相當(dāng)于是一個系統(tǒng)調(diào)用,會觸發(fā)內(nèi)核狀態(tài)切換。在用戶態(tài)調(diào)用 mach_msg_trap()
時會切換到內(nèi)核態(tài);內(nèi)核態(tài)中內(nèi)核實(shí)現(xiàn)的mach_msg()
函數(shù)會完成實(shí)際的工作。
即基于port的source1,監(jiān)聽端口,端口有消息就會觸發(fā)回調(diào);而source0,要手動標(biāo)記為待處理和手動喚醒RunLoop
Mach消息發(fā)送機(jī)制
大致邏輯為:
1、通知觀察者 RunLoop 即將啟動。
2、通知觀察者即將要處理Timer事件。
3、通知觀察者即將要處理source0事件。
4、處理source0事件。
5、如果基于端口的源(Source1)準(zhǔn)備好并處于等待狀態(tài),進(jìn)入步驟9。
6、通知觀察者線程即將進(jìn)入休眠狀態(tài)。
7、將線程置于休眠狀態(tài),由用戶態(tài)切換到內(nèi)核態(tài),直到下面的任一事件發(fā)生才喚醒線程。
- 一個基于 port 的Source1 的事件(圖里應(yīng)該是source0)。
- 一個 Timer 到時間了。
- RunLoop 自身的超時時間到了。
- 被其他調(diào)用者手動喚醒。
8、通知觀察者線程將被喚醒。
9、處理喚醒時收到的事件。
- 如果用戶定義的定時器啟動,處理定時器事件并重啟RunLoop。進(jìn)入步驟2。
- 如果輸入源啟動,傳遞相應(yīng)的消息。
- 如果RunLoop被顯示喚醒而且時間還沒超時,重啟RunLoop。進(jìn)入步驟2
10、通知觀察者RunLoop結(jié)束。
Runloop從執(zhí)行上分析
所謂 Runloop,簡而言之,是 Apple 所設(shè)計(jì)的,一種在當(dāng)前線程,持續(xù)調(diào)度各種任務(wù)的運(yùn)行機(jī)制。說起來有些繞口,我們翻譯成代碼就非常直白了。
while (alive) {
performTask() //執(zhí)行任務(wù)
callout_to_observer() //通知外部
sleep() //休眠
}
每一次 loop 執(zhí)行,主要做三件事:
performTask()
callout_to_observer()
sleep()
performTask
每一次 runloop 的運(yùn)行都會執(zhí)行若干個 task,執(zhí)行 task 的方式有多種,有些方式可以被開發(fā)者使用,有些則只能被系統(tǒng)使用。逐一看下:
DoBlocks()
這種方式可以被開發(fā)者使用,使用方式很簡單。可以先通過 CFRunLoopPerformBlock 將一個 block 插入目標(biāo)隊(duì)列,函數(shù)簽名如下:
void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void (^block)(void));
詳細(xì)使用方式可參考文檔:https://developer.apple.com/documentation/corefoundation/1542985-cfrunloopperformblock?language=objc
可以看出該 block 插入隊(duì)列的時候,是綁定到某個 runloop mode 的,runloop mode 的概念后面會詳細(xì)解釋,也是理解 runloop 運(yùn)行機(jī)制的關(guān)鍵。
調(diào)用上面的 api 之后,runloop 在執(zhí)行的時候,會通過如下 API 執(zhí)行隊(duì)列里所有的 block:
__CFRunLoopDoBlocks(rl, rlm);
很顯然,執(zhí)行的時候也是只執(zhí)行和某個 mode 相關(guān)的所有 block。至于執(zhí)行的時機(jī)點(diǎn)有多處,后面也會標(biāo)注。
DoSources0()
CFRunloopSource
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits; //用于標(biāo)記Signaled狀態(tài),source0只有在被標(biāo)記為Signaled狀態(tài),才會被處理
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
Runloop 里有兩種 source,CFRunLoopSource 是對 input sources 的抽象,它要么是 source0,那么是 source1。source0 和 source1,雖然名稱相似,二者運(yùn)行機(jī)理并不相同。source0 有公開的 API 可供開發(fā)者調(diào)用,source1 卻只能供系統(tǒng)使用,而且 source1 的實(shí)現(xiàn)原理是基于 mach_msg 函數(shù),通過讀取某個 port 上內(nèi)核消息隊(duì)列上的消息來決定執(zhí)行的任務(wù)。
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
void (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
void (*perform)(void *info);
} CFRunLoopSourceContext;
作為開發(fā)者要使用 source0 也很簡單,先創(chuàng)建一個 CFRunLoopSourceContext,context 里需要傳入被執(zhí)行任務(wù)的函數(shù)指針作為參數(shù),再將該 context 作為構(gòu)造參數(shù)傳入 CFRunLoopSourceCreate 創(chuàng)建一個 source,之后通過 CFRunLoopAddSource 將該 source 綁定的某個 runloop mode 即可。
詳細(xì)文檔可參考:https://developer.apple.com/documentation/corefoundation/1542679-cfrunloopsourcecreate?language=objc
綁定好之后,runloop 在執(zhí)行的時候,會通過如下 API 執(zhí)行所有的 source0:
__CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
同理,每次執(zhí)行的時候,也只會運(yùn)行和當(dāng)前 mode 相關(guān)的 source0。
DoSources1()
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
mach_port_t (*getPort)(void *info);
void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
void * (*getPort)(void *info);
void (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;
如上所述,source1 并不對開發(fā)者開放,系統(tǒng)會使用它來執(zhí)行一些內(nèi)部任務(wù),比如渲染 UI。
公司內(nèi)部有個厲害的工具,可以將某個線程一段時間內(nèi)所執(zhí)行的函數(shù)全部 dump 下來,上傳到后臺并以流程圖的形式展示,很直觀。得益于這個工具,我可以清楚的看到 DoBlocks,DoSources0, DoSources1 被使用時的 call stack,也就能知道系統(tǒng)是處于什么目的在使用上述三種任務(wù)調(diào)用機(jī)制,后面解釋。
DoTimers()
這個比較簡單,開發(fā)者使用 NSTimer 相關(guān) API 即可注冊被執(zhí)行的任務(wù),runloop 通過如下 API 執(zhí)行相關(guān)任務(wù):
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
同理,每次執(zhí)行的時候,也只會運(yùn)行和當(dāng)前 mode 相關(guān)的 timer。
DoMainQueue()
這個也再簡單不過,開發(fā)者調(diào)用 GCD 的 API 將任務(wù)放入到 main queue 中,runloop 則通過如下 API 執(zhí)行被調(diào)度的任務(wù):
_dispatch_main_queue_callback_4CF(msg);
注意,這里就沒有 rlm 參數(shù)了,也就是說 DoMainQueue 和 runloop mode 是不相關(guān)的。msg 是通過 mach_msg 函數(shù)從某個 port 上讀出的 msg。
問題來了
綜上所述,在 runloop 里一共有 5 種方式來執(zhí)行任務(wù),那么問題來了,蘋果為什么要搞這么多花樣,他們各自的使用場景是什么?
timer 和 mainqueue 無需多說,開發(fā)者大多熟悉其背后設(shè)計(jì)宗旨。至于 DoBlocks,DoSources0,和 DoSources1,我原先以為系統(tǒng)在使用時,他們各有分工,比如某些用來接收硬件事件,有些則負(fù)責(zé)渲染 Core Animation 任務(wù),但實(shí)際觀摩過一些主線程運(yùn)行樣本之后,我發(fā)現(xiàn)并無類似的 pattern。
比如我在 doSource0 里看到了這個 callstack:
..__CFRunLoopDoSources0 ...[UIApplication sendEvent:] ...
顯然是系統(tǒng)用 source0 任務(wù)來接收硬件事件。
又比如這個使用 mainqueue 的 callstack:
..._dispatch_main_queue_callback_4CF...[UIView(Hierarchy) _makeSubtreePerformSelector:withObject:withObject:copySublayers:]...
系統(tǒng)在使用 doMainQueue 來執(zhí)行 UIView 的布局任務(wù)。
再比如這個 callstack:
...__CFRunLoopDoBlocks...CA::Context::commit_transaction(CA::Transaction*)...
這是系統(tǒng)在使用 doBlocks 來提交 Core Animation 的繪制任務(wù)。
繼續(xù)看這個:
...__CFRunLoopDoSources0...CA::Transaction::commit() ...
這是系統(tǒng)在使用 doSource0 來提交 Core Animation 的繪制任務(wù)。
不知道大家看出什么 pattern 沒,我沒,唯一比較有規(guī)律的是硬件事件都是通過 doSource0 來傳遞的,總體感覺系統(tǒng)在使用的時候有點(diǎn) free style。
callout_to_observer
這一分類主要是 runloop 用來通知外部 observer 用的,用來告知外部某個任務(wù)已被執(zhí)行,或者是 runloop 當(dāng)前處于什么狀態(tài)。我們也來逐一看下:
DoObservers-Timer
故名思義,在 DoTimers 執(zhí)行完畢之后,調(diào)用 DoObservers-Timer 來告知感興趣的 observer,怎么注冊 observer 呢?在介紹完各種 callback 機(jī)制之后,再統(tǒng)一說下。runloop 是通過如下函數(shù)來通知 observer:
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);復(fù)制代碼
DoObservers-Source0
同理,是在執(zhí)行完 source0 之后,調(diào)用 DoObservers-Source0 來告知感興趣的 observer,怎么注冊后面統(tǒng)一介紹。runloop 通過如下函數(shù)來通知 observer:
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
這是上述五種執(zhí)行任務(wù)方式中,兩種可以注冊 observer 的,其他幾個都不支持,mainQueue,source1,block 都不行。所以理論上,是沒有辦法準(zhǔn)確測量各個任務(wù)執(zhí)行的時長的。
DoObservers-Activity
這是 runloop 用來通知外部自己當(dāng)前狀態(tài)用的,當(dāng)前 runloop 正執(zhí)行到哪個 activity,那么一共有幾種 activity 呢?看源碼一清二楚:
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
};
啰嗦下,再一個個講解:
kCFRunLoopEntry
每次 runloop 重新進(jìn)入時的 activity,runloop 每一次進(jìn)入一個 mode,就通知一次外部 kCFRunLoopEntry,之后會一直以該 mode 運(yùn)行,知道當(dāng)前 mode 被終止,進(jìn)而切換到其他 mode,并再次通知 kCFRunLoopEntry。runloop mode 的切換也是個很有意思的話題,后面會提到。
kCFRunLoopBeforeTimers
這就是上面提到的 DoObservers-Timer,Apple 應(yīng)該是為了代碼的整潔,將 kCFRunLoopBeforeTimers 也歸為了一種 activity,其含義上面已經(jīng)介紹,不再贅述。
kCFRunLoopBeforeSources
同理,Apple 也將該 callout 歸為了一種 runloop 的 activity。
kCFRunLoopBeforeWaiting
這個 activity 表示當(dāng)前線程即將可能進(jìn)入睡眠,如果能夠從內(nèi)核隊(duì)列上讀出 msg 則繼續(xù)運(yùn)行任務(wù),如果當(dāng)前隊(duì)列上沒多余消息,則進(jìn)入睡眠狀態(tài)。讀取 msg 的函數(shù)為:
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), poll ? 0 : TIMEOUT_INFINITY);復(fù)制代碼
其本質(zhì)是調(diào)用了開篇所說的 mach_msg 內(nèi)核函數(shù),注意 timeout 值,TIMEOUT_INFINITY 表示有可能無限進(jìn)入睡眠狀態(tài)。
kCFRunLoopAfterWaiting
這個 activity 是當(dāng)前線程從睡眠狀態(tài)中恢復(fù)過來,也就是說上面的 mach_msg 終于從隊(duì)列里讀出了 msg,可以繼續(xù)執(zhí)行任務(wù)了。這是每一次 runloop 從 idle 狀態(tài)中恢復(fù)必調(diào)的一個 activity,如果你想設(shè)計(jì)一個工具檢測 runloop 的執(zhí)行周期,那么這個 activity 就可以作為周期的開始。
kCFRunLoopExit
exit 不必多言,切換 mode 的時候可能會調(diào)用到這個 activity,為什么說可能呢?這和 mode 切換的方式有關(guān),后面會提及。
activity 的 回調(diào)并不是單單給開發(fā)者用的,事實(shí)上,系統(tǒng)也會通過注冊相關(guān) activity 的回調(diào)來完成一些任務(wù),比如我看到過如下的 callstack:
...__CFRunLoopDoObservers...[UIView(Hierarchy) addSubview:] ...
顯然系統(tǒng)在觀測到 runloop 進(jìn)入某個 activity 之后,會進(jìn)行一些 UIView 的布局工作。
再看這個:
...__CFRunLoopDoObservers...[UIViewController __viewWillDisappear:] ...
這是系統(tǒng)在使用 DoObservers 傳遞 viewWillDisappear 回調(diào)。
以上即為 observer 的全部內(nèi)容,一般開發(fā)者對 runloop 的 activity 感興趣,多半是想分析主線程的業(yè)務(wù)代碼執(zhí)行情況,事實(shí)上,這些 activity 的回調(diào)不怎么可靠,也就是說有可能 runloop 哼哧運(yùn)行來半天的代碼,你一個 activity 的回調(diào)也收不到,或者收到了,但順序也是完全出乎你的意料,后面會詳細(xì)解釋。
sleep
一言以蔽之,有任務(wù)就執(zhí)行,沒任務(wù)就 sleep。這部分邏輯就這么簡單。
只是有個小細(xì)節(jié)需要注意,一般人印象里感覺 runloop 的每次 loop 總是按順序執(zhí)行上面的各種 performTask 和 callout_to_observer,執(zhí)行完就 sleep,而實(shí)際上,這些任務(wù)的執(zhí)行相互糅合在一起,還有 goto 的跳轉(zhuǎn)邏輯,顯得非常凌亂,而且 activity 的 callback 也可能不是按照 kCFRunLoopEntry->kCFRunLoopBeforeWaiting->kCFRunLoopAfterWaiting->kCFRunLoopExit 來的,后面我會畫個流程圖來解釋下。
Runloop 的 loop 主函數(shù)為 __CFRunLoopRun,里面的這行調(diào)用會決定是否 sleep:
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), poll ? 0 : TIMEOUT_INFINITY);
其內(nèi)部無非是使用了我們開篇所提到的 mach_msg 函數(shù)。
完整流程
至此,我們已將 runloop 中的關(guān)鍵代碼分為了三類,并就這三類進(jìn)行了展開,接下來我們看下完整的流程。
Apple 工程師提到 runloop 的實(shí)現(xiàn)可能會隨著 iOS 版本而變化,我在對比 Objective C 和 Swift 版本代碼之后,發(fā)現(xiàn)關(guān)鍵流程沒多少區(qū)別,下面這張圖是我閱讀代碼時順手繪制的,希望能讓讀者對 runloop 的運(yùn)行機(jī)制有更直觀形象的認(rèn)識:
我將 performTask 和 callout_to_observer 用不同的顏色加以了區(qū)分,從圖中可以直觀的看到 5 種 performTask 和 6 種 callout_to_observer 是在一次 loop 里如何分布的。
有些細(xì)節(jié)難以在圖中體現(xiàn),再單獨(dú)拿出來解釋下。
Poll?
每次 loop 如果處理了 source0 任務(wù),那么 poll 值會為 true,直接的影響是不會 DoObservers-BeforeWaiting 和 DoObservers-AfterWaiting,也就是說 runloop 會直接進(jìn)入睡眠,而且不會告知 BeforeWaiting 和 AfterWaiting 這兩個 activity。所以你看,有些情況下,可能 runloop 經(jīng)過了幾個 loop,但你注冊的 observer 卻不會收到 callback。
兩次 mach_msg
其實(shí)一次 loop 里有兩次調(diào)用 mach_msg,有一次我沒有標(biāo)記出來,是發(fā)生在 DoSource0 之后,會主動去讀取和 mainQueue 相關(guān)的 msg 隊(duì)列,這不過這個 mach_msg 調(diào)用是不會進(jìn)入睡眠的,因?yàn)?timeout 值傳入的是 0,如果讀到了消息,就直接 goto 到 DoMainQueue 的代碼,這種設(shè)計(jì)應(yīng)該是為了保障 dispatch 到 main queue 的代碼總是有較高的機(jī)會得以運(yùn)行。
Port Type
每次 runloop 被喚醒之后,會根據(jù) port type 而決定到底執(zhí)行哪一類任務(wù),DoMainQueue,DoTimers,DoSource1 三者只會運(yùn)行一個,剩下的會留到下一次 loop 里去執(zhí)行。
Runloop Mode
接下來是關(guān)鍵里的重點(diǎn),重點(diǎn)里的核心,關(guān)于 runloop mode 的理解。
開始之前,再回顧下 runloop 在一次 loop 里可能會做的事情,代碼如下:
while (alive) {
//執(zhí)行任務(wù)
DoBlocks();
DoSources0();
DoSources1();
DoTimers();
DoMainQueue();
//通知外部
DoObservers-Timer();
DoObservers-Source0();
DoObservers-Activity();
//休眠
sleep() }
Runloop mode 的設(shè)計(jì)就是為了執(zhí)行上述的邏輯服務(wù),我反復(fù)提到過,大部分的任務(wù)和回調(diào)是和 mode 綁定的,那么我們來看下 mode 的數(shù)據(jù)結(jié)構(gòu)是如何體現(xiàn)這部分功能的:
struct __CFRunLoopMode {
...
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
CFIndex _observerMask;
...};
為了閱讀方便,我略去了一些不太相關(guān)的細(xì)節(jié)。很容易看出,執(zhí)行任務(wù)和通知外部所需要的信息全都定義在了 mode 的數(shù)據(jù)結(jié)構(gòu)里,基本上都是一個 array 來持有相關(guān)引用,比如當(dāng)前 loop 需要 DoTimers() 的時候,只需要將 _timers 遍歷并 invoke 即可:
static Boolean __CFRunLoopDoTimers(CFRunLoopRef rl, CFRunLoopModeRef rlm, int64_t limitTSR) {/* DOES CALLOUT */
for (CFIndex idx = 0, cnt = rlm->_timers ? CFArrayGetCount(rlm->_timers) : 0; idx < cnt; idx++) {
...
}
return timerHandled;}
_observerMask 包含所有 observer 感興趣的 activity,每次 observer 通過如下 API 創(chuàng)建一個新的 activity callback 并注冊的時候, mask 也會隨之更新:
CFRunLoopObserverRef CFRunLoopObserverCreateWithHandler(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, void (^block) (CFRunLoopObserverRef observer, CFRunLoopActivity activity))
而 mainQueue 任務(wù)的執(zhí)行和 mode 無關(guān),所以 mode 的結(jié)構(gòu)定義里并無 mainQueue 相關(guān)的信息。
其他都比較直白,無須多言。
Runloop Mode 的種類
關(guān)于 Runloop Mode 的種類以及其背后設(shè)計(jì)思想,沒有太多的文檔可以參考,但這部分信息卻至關(guān)重要。
簡單來說 mode 分為兩類,common mode 和 private mode。
比如我們所熟知的 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 是屬于 common mode,kCFRunLoopDefaultMode 是默認(rèn)模式下 runloop 使用的 mode,scrollView 滑動的時候切換到 UITrackingRunLoopMode。
除此之外,系統(tǒng)還定義了其他一些 private mode,比如 UIInitializationRunLoopMode 和 GSEventReceiveRunLoopMode。如果你在 app 啟動的時候通過 CFRunLoopCopyAllModes 打印出所有的 runloop mode,就可以看到這兩個 mode 了。我們簡單探討下 GSEventReceiveRunLoopMode 的使用場景。
GSEventReceiveRunLoopMode 以 GS 開頭,是屬于 GraphicsServices 這個并不公開的 framework,GSEvent 封裝了系統(tǒng)傳遞給 app 的重要事件,比如音量按鈕事件,屏幕點(diǎn)擊事件等,而我們所熟知的 UIEvent 也不過是 GSEvent 的封裝。我曾一度懷疑 apple 會使用 GSEventReceiveRunLoopMode 來傳遞各類系統(tǒng)事件,可惜的是,我在線上代碼里設(shè)置里一段捕捉邏輯,上報(bào)所有未知的 runloop mode,卻并沒有捕獲到 GSEventReceiveRunLoopMode 的使用場景。之后出于好奇,使用了一次召喚神龍的機(jī)會,給 Apple 工程師提了個 TSL,接我單的小哥只是隱晦的承認(rèn)了 GSEventReceiveRunLoopMode 的存在,并表示這事不能說太細(xì),Apple 的確會在一些場景下基于需要使用一些 private mode,事實(shí)上,開發(fā)者自己也可以創(chuàng)建 private mode 來實(shí)現(xiàn)一些功能,比如這個 post 里的例子:https://forums.developer.apple.com/message/187122#187122。除此之外,我并沒有得到其他什么有用的信息,有點(diǎn)想退貨。
這篇文檔列舉了一些公開的 mode:http://iphonedevwiki.net/index.php/CFRunLoop。
我設(shè)置的捕捉代碼也捕獲到了另一些有意思的 mode,比如這個 _kCFStreamBlockingOpenMode,google 一下,這是 CFStream 里用來調(diào)度網(wǎng)絡(luò)任務(wù)所使用的 private mode,源碼 https://opensource.apple.com/source/CF/CF-476.19/CFStream.c.auto.html。
問題來了
runloop mode 分為 common 和 private 對我們?nèi)粘I钣心男┯绊懩兀坑绊懞艽蟆?/p>
當(dāng)我們對 runloop 的 activity 感興趣,并通過如下 API 注冊 observer 的時候
CF_EXPORT void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);
大多數(shù)時候我們都會傳入 kCFRunLoopCommonModes 作為參數(shù),這也就意味著你的 observer 只會在 common mode 被運(yùn)行的時候 call back,如果當(dāng)前 loop 是以 private mode 運(yùn)行的,那么你的 observer 將對 runloop 當(dāng)前的 activity 渾然不覺。如果你的代碼強(qiáng)依賴于 runloop activity 的監(jiān)測,這顯然會成為一個關(guān)鍵缺陷。private mode 使用的場景之多可能超過你的想象。
簡而言之,每次 loop 只會以一種 mode 運(yùn)行,以該 mode 運(yùn)行的時候,就只執(zhí)行和該 mode 相關(guān)的任務(wù),只通知該 mode 注冊過的 observer。
runloop mode 是如何切換的呢?
這個問題涉及到 runloop 的 mode 到底是如何使用的,顯然我們無法得知系統(tǒng)是如何使用的,就如同那些 Apple 諱莫如深的 private mode。好在我們還是可以從代碼得出分析。
每次如果要切換 mode,為了保證多線程安全,必會先通過如下代碼 lock:
__CFRunLoopLock(rl);__CFRunLoopModeLock(rlm);
切換完之后再 unlock。
而整個runloop 關(guān)鍵流程函數(shù)里,主要有三處 unlock 的調(diào)用。
一處是在 sleep 之前,runloop 可能一覺醒來,發(fā)現(xiàn) mode 已經(jīng)物是人非。
另一處是在 doMainQueue 之前,執(zhí)行完 GCD main queue 中的任務(wù)后,mode 也能會發(fā)生變化。
最后一處是在 CFRunLoopRunSpecific 函數(shù),也就是 runloop exit 之后。
所以我們可以得出結(jié)論,runloop 有兩種切換 mode 的方式,一是在 loop 的中途切換,二是按順序在當(dāng)前 mode 結(jié)束之后切換。
如果你也對 mode 的使用比較感興趣,真相都在下面這三個可供開發(fā)者使用的函數(shù)里:
CF_EXPORT CFRunLoopMode CFRunLoopCopyCurrentMode(CFRunLoopRef rl);CF_EXPORT CFArrayRef CFRunLoopCopyAllModes(CFRunLoopRef rl);CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode)
RunLoop相關(guān)應(yīng)用場景
RunLoop與NSTimer
一個比較常見的問題:滑動tableView時,定時器還會生效嗎?
默認(rèn)情況下RunLoop運(yùn)行在kCFRunLoopDefaultMode下,而當(dāng)滑動tableView時,RunLoop切換到UITrackingRunLoopMode,而Timer是在kCFRunLoopDefaultMode下的,就無法接受處理Timer的事件。
怎么去解決這個問題呢?把Timer添加到UITrackingRunLoopMode上并不能解決問題,因?yàn)檫@樣在默認(rèn)情況下就無法接受定時器事件了。
所以我們需要把Timer同時添加到UITrackingRunLoopMode和kCFRunLoopDefaultMode上。
那么如何把timer同時添加到多個mode上呢?就要用到NSRunLoopCommonModes了
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Timer就被添加到多個mode上,這樣即使RunLoop由kCFRunLoopDefaultMode切換到UITrackingRunLoopMode下,也不會影響接收Timer事件
RunLoop和線程
線程和RunLoop是一一對應(yīng)的,其映射關(guān)系是保存在一個全局的 Dictionary 里
自己創(chuàng)建的線程默認(rèn)是沒有開啟RunLoop的
怎么創(chuàng)建一個常駐線程?
1、為當(dāng)前線程開啟一個RunLoop(第一次調(diào)用 [NSRunLoop currentRunLoop]方法時實(shí)際是會先去創(chuàng)建一個RunLoop)
1、向當(dāng)前RunLoop中添加一個Port/Source等維持RunLoop的事件循環(huán)(如果RunLoop的mode中一個item都沒有,RunLoop會退出)
2、啟動該RunLoop
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
輸出下邊代碼的執(zhí)行順序
NSLog(@"1");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
[self performSelector:@selector(test) withObject:nil afterDelay:10];
NSLog(@"3");
});
NSLog(@"4");
- (void)test
{
NSLog(@"5");
}
答案是1423,test方法并不會執(zhí)行。
原因是如果是帶afterDelay的延時函數(shù),會在內(nèi)部創(chuàng)建一個 NSTimer,然后添加到當(dāng)前線程的RunLoop中。也就是如果當(dāng)前線程沒有開啟RunLoop,該方法會失效。
那么我們改成:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
[[NSRunLoop currentRunLoop] run];
[self performSelector:@selector(test) withObject:nil afterDelay:10];
NSLog(@"3");
});
然而test方法依然不執(zhí)行。
原因是如果RunLoop的mode中一個item都沒有,RunLoop會退出。即在調(diào)用RunLoop的run方法后,由于其mode中沒有添加任何item去維持RunLoop的時間循環(huán),RunLoop隨即還是會退出。
所以我們自己啟動RunLoop,一定要在添加item后
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
[self performSelector:@selector(test) withObject:nil afterDelay:10];
[[NSRunLoop currentRunLoop] run];
NSLog(@"3");
});
怎么保證子線程數(shù)據(jù)回來更新UI的時候,不打斷用戶的滑動操作?
滑動是在UITrackingRunloopMode下,滑動結(jié)束了,runloop由UITrackingRunloopMode又回到defaultMode下了。
數(shù)據(jù)加載一般在子線程下載,下載完畢后在主線程進(jìn)行UI刷新。
可以將子線程數(shù)據(jù),給主線程刷新UI的時候,包裝后提交到主線程的defaultModel下,這樣兩個model不會同時執(zhí)行,也就不會打斷用戶的滑動操作。
[self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
參考:
https://blog.ibireme.com/2015/05/18/runloop/
https://zhuanlan.zhihu.com/p/64593559
https://blog.csdn.net/u014795020/article/details/72084735
http://www.lxweimin.com/p/fcb271f69038
https://www.cnblogs.com/kenshincui/p/6823841.html
https://blog.csdn.net/gsl111000/article/details/99311163
https://blog.csdn.net/csdn_coder_zxq/article/details/90740239
http://www.cocoachina.com/cms/wap.php?action=article&id=25906