翻譯自 Run, RunLoop, Run!html
儘管在開發者間不多討論,但它是全部 app 中最重要的幾個組件之一:Run Loops。Run Loops 就像是 app 跳動的心臟,它是讓你的代碼真正運行起來的東西。java
Run Loop 的基本原理實際上很簡單。在 iOS 和 OSX 中,CFRunLoop
實現了供全部高層通訊和分發 API 使用的的核心機制。android
簡單來講,Run Loop 是一種通訊機制,用來完成異步或線程間通信。能夠把它看做一個郵箱——等待消息並將它們發送給接收者們。git
Run Loop 要作兩件事:github
在其餘平臺中(Win32),這種機制被叫作「消息泵(Message Pump)」。c#
Run Loop 是區分交互式應用和命令行工具的關鍵。命令行工具接受參數並啓動,執行具體命令,最終退出。交互式應用則等待用戶輸入,作相應的反應,而後接着等待。事實上,這個基本機制在長時間運行的程序中也很常見。好比在服務器中的 while(1) {select();}
就是一個很好(雖然老)的 Run Loop 例子。服務器
Run loop 的工做就是等待某些事情發生。這些事情能夠是由用戶或者系統觸發的外部事件(例如網絡請求),或者是內部應用消息,例如線程間通知、異步代碼執行、計時器等等。當收到一個事件(或者說消息)時,Run loop 找到一個相應的監聽者並將消息傳遞給它。網絡
實現一個基礎的 Run loop 很容易。下面是一個簡單的僞代碼版本:閉包
func postMessage(runloop, message){
runloop.queue.pushBack(message)
runloop.signal()
}
func run(runloop){
do {
runloop.wait()
message = runloop.queue.popFront()
dispatch(message)
} while (true)
}
複製代碼
用這種簡單的機制,每一個線程都會 run()
本身的 Run loop,而後使用 postMessage()
來和其餘線程異步交換消息。個人同事 Cyril Mottier 告訴我 Android 版本的實現 並不比這個複雜多少。app
在 Apple 系統裏面,這由 CFRunLoop
實現,一個稍微高級點的變形(CFRunLoop.c 有 3909 行,Looper.java 有 309 行)。除了早期初始化和你本身生成的線程,全部你寫的代碼都會在某個時刻被 CFRunLoop
調用。(據我所知,爲 GCD 自動建立的線程不須要 CFRunLoop
,可是確定有一個消息系統來容許複用。)CFRunLoop
最重要的特性是 CFRunLoopModes
。CFRunLoop
與一個「Run Loop Sources」系統一同工做。註冊在 Run Loop 上的 Sources 有一個或多種模式(modes),而 Run loop 自己就是在給定模式下運行的。當一個事件到達 source 時,只會交給有和 source 的模式匹配的 Run loop 來處理。
此外,CFRunLoop 是可重入的,不管是經過本身的代碼或者框架內部代碼。由於每一個線程都只有一個 CFRunLoop
,當一個組件想在一個特定模式下運行 Run Loop ,能夠經過調用 CFRunLoopRunInMode()
實現。全部沒有被註冊爲這個模式的 Run Loop source 都將被中止處理。一般該組件最終還會返回以前的模式。
CFRunLoop定義了一個僞模式:「公共模式」(kCFRunLoopCommonModes
),其實是一組「常規」的 Run Loop。主 Run Loop 開始是工做在 kCFRunLoopCommonModes
。另外一方面,UIKit 定義了一個特殊的 Run Loop 模式叫作 UITrackingRunLoopMode
。它在「當控制跟蹤發生時」使用這個模式,例如觸摸的時候。這很是重要,由於這保證了 TableView 的流暢滾動。當主線程的 Run Loop 在 UITrackingRunLoopMode
時,大部分後臺事件,例如網絡回調,都沒有被分發。這樣,沒有了額外處理,滾動就不會卡頓(如今再卡頓的話,就是你的錯了)。
若是你用堆棧跟蹤調試過 iOS 和 OS X 代碼,極可能你會在棧跟蹤中注意到一個全大寫的方法 CFRUNLOOP_IS_CALLING_OUT
。當 CFRunLoop
調用程序代碼時,它就喜歡這麼幹。這裏列出了 6 個定義在 CFRunLoop.c 的函數:
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__();
複製代碼
你猜的沒錯,這些函數除了用於跟蹤調試外沒其餘做用。 CFRunLoop
確保了全部的應用代碼會經過上面其中一個函數調用。讓咱們一個一個地看一下。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(
CFRunLoopObserverCallBack func,
CFRunLoopObserverRef observer,
CFRunLoopActivity activity,
void *info);
複製代碼
觀察者(Observers) 有一些特別。CFRunLoopObserver
API 容許你觀察 CFRunLoop
的行爲和它是否活躍(在處理事件或是正要去休眠等)。觀察者在調試時很是有用,尤爲是當你想了解 CFRunLoop
的特性的話。事實上,在一些特定的用途上它頗有用,例如:CoreAnimation
經過觀察者調出(callout)來運行,這是有意義的,由於這樣保證了全部 UI 代碼都已經被執行,而且一次執行完全部的動畫。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(void (^block)(void));
複製代碼
閉包(Blocks) 是 CFRunLoopPerformBlock()
API 的 另外一面,當你想在「下一個循環」運行代碼時它很是有用。
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(void *msg);
複製代碼
Main Dispatch Queue 標籤是 CFRunLoop 對 GCD 的處理。顯然,至少在主線程上,GCD 和 CFRunLoop
是協同工做的。即便 GCD 能夠(而且會)創造沒有 CFRunLoop
的線程,但當這裏有一個 CFRunLoop
時,它會插入進去。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(
CFRunLoopTimerCallBack func,
CFRunLoopTimerRef timer,
void *info);
複製代碼
定時器(Timer) 相對容易從字面理解。在 iOS 和 OSX 中,高層 「定時器」 例如 NSTimer
或者 performSelector:afterDelay:
是經過 CFRunLoop
定時器實現的。從 iOS 7 和 Mavericks 開始,定時器的觸發時間點有了一個容錯區間的概念,這個特性固然也是 CFRunLoop
處理的。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(
void (*perform)(void *),
void *info);
複製代碼
CFRunLoopSources
「Version 0」和「Version 1」是兩個很是不一樣的東西,雖然它們有一個通用的 API。
Version 0 sources 只是一個應用內消息處理機制,必須由應用代碼手動處理。在給 Version 0 Source 發送信號後(經過 CFRunLoopSourceSignal()
),CFRunloop
必須被喚醒(經過 CFRunLoopWakeUp()
)後才能處理這個 source。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(
void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info),
mach_msg_header_t *msg,
CFIndex size,
mach_msg_header_t **reply,
void (*perform)(void *),
void *info);
複製代碼
另外一方面,Version 1 Sources 用 mach_port
來處理內核事件。這其實是 CFRunLoop
的核心:大多數時候,當你的 app 就站在那,什麼也不作的時候,它會被阻塞在這個 mach_msg(…,MACH_RCV_MSG,…)
調用中。若是你用活動監視器(Activity Monitor)觀察任意一個 app,極可能你會看到這個:
2718 CFRunLoopRunSpecific (in CoreFoundation) + 296 [0x7fff98bb7cb8]
2718 __CFRunLoopRun (in CoreFoundation) + 1371 [0x7fff98bb845b]
2718 __CFRunLoopServiceMachPort (in CoreFoundation) + 212 [0x7fff98bb8f94]
2718 mach_msg (in libsystem_kernel.dylib) + 55 [0x7fff99cf469f]
2718 mach_msg_trap (in libsystem_kernel.dylib) + 10 [0x7fff99cf552e]
複製代碼
就在 CGRunLoop.c 的這裏。在上面幾行,你能看到 Apple 工程師引用了哈姆雷特的獨白:
/* In that sleep of death what nightmares may come ... */
複製代碼
每當你的 app 運行時,CFRunLoop
的核心是 __CFRunLoopRun()
函數,經過公有 API CFRunLoopRun()
和 CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled)
來調用。
__CFRunLoopRun()
會由於四個緣由退出:
kCFRunLoopRunTimedOut
: 超時時,若是指定了間隔kCFRunLoopRunFinished
: 當它 「空」的時候,例如全部的資源都被刪除了kCFRunLoopRunHandledSource
: 若是有 returnAfterSourceHandled
標誌,在事件被髮送以後kCFRunLoopRunStopped
: 經過 CFRunLoopStop()
手動中止在以上四個緣由之一出現以前,它會一直等待和分發事件。下面是一個包含咱們前文討論的各類類型事件的處理過程的例子。
CFRunLoopPerformBlock()
API)。mach_port
,而後#ifdef
#elif
代碼(b)代碼中間有 goto
。主要的思路是,能夠將 mach_msg()
配置爲在多個隊列和端口上等待。CFRunLoop
能夠同時等待定時器、GCD 分發、手動喚醒或是去處理 Version 1 Sources。很簡單吧?CoreFoundation
是由 C 實現的,看起來不怎麼現代。我看到代碼的第一反應是「這須要重構」。但另外一方面這些代碼久經沙場,因此我不期待最近它會被用 Swift 重寫。
有一種代碼模式,我最近幾年一直在使用,特別是在測試中。它就是「運行 run loop,直到這個條件變爲真」,這是任何類型的異步單元測試的基礎。隨着時間的推移,我可能已經編寫了不少這樣的變體,直接使用 NSRunLoop
或 CFRunLoop
,進行輪詢,使用超時等等。如今我能夠編寫一個像樣的版本了,讓咱們在下一篇文章中找到答案。