在學習完內存管理與多線程的知識後,我又將目光瞄向了 Run Loop,不過受限於現階段的能力,我在查閱了大量資料後,對於 Run Loop 的理解仍然很是淺顯,因此本文絕大多數的內容,是參照網上大牛們的文章進行總結的。固然啦,我也但願在不久的未來,對於 Run Loop 能有更多本身的觀點與總結。html
首先看如下代碼:前端
1 |
int main(int argc, char * argv[]) { |
不知道剛接觸 iOS 開發的同窗有沒有過這樣的疑惑:咱們都知道 main
函數是程序的入口,可爲什麼當 main
函數執行完畢後,程序沒有退出呢?而能在沒有事情作的時候維持應用的運行的呢?ios
若是你是個好奇的寶寶,那麼必定會去搜尋答案,沒錯,其實這背後便隱藏了今天的主角 Run Loop。segmentfault
如下來自蘋果官方文檔的介紹:安全
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.網絡
Run loop management is not entirely automatic. You must still design your thread’s code to start the run loop at appropriate times and respond to incoming events. Both Cocoa and Core Foundation provide run loop objects to help you configure and manage your thread’s run loop. Your application does not need to create these objects explicitly; each thread, including the application’s main thread, has an associated run loop object. Only secondary threads need to run their run loop explicitly, however. The app frameworks automatically set up and run the run loop on the main thread as part of the application startup process.多線程
通常來說,一個線程一次只能執行一個任務,執行完成後線程就會退出。若是咱們須要一個機制,讓線程能隨時處理事件但並不退出,那麼就得讓它循環。架構
因此,Run Loop 實際上就是一個對象,這個對象管理了其須要處理的事件和消息,並提供了一個入口函數來執行任務。線程執行了這個函數後,就會一直處於這個函數內部 「接受消息->等待->處理」 的循環中,直到這個循環結束(好比傳入 quit 的消息),函數返回。app
因此,上面代碼中 UIApplicationMain()
方法在這裏不只完成了初始化咱們的程序並設置程序 Delegate 的任務,並且隨之開啓了主線程的 Run Loop,開始接受處理事件。這樣咱們的應用就能夠在無人操做的時候休息,須要讓它幹活的時候又能立馬響應。框架
直接看圖更容易理解:
在 OS X/iOS 系統中,提供了兩個這樣的對象:
• CFRunLoopRef:是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,全部這些 API 都是線程安全的。
• NSRunLoop:是基於 CFRunLoopRef 的封裝,提供了面向對象的 API,可是這些 API 不是線程安全的。
首先來看一張關係圖:
蘋果不容許直接建立 Run Loop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
,這兩個函數內部的邏輯大概是下面這樣:
1 |
/// 全局的 Dictionary,key 是 pthread_t, value 是 CFRunLoopRef |
從上面的代碼能夠看出,線程和 Run Loop 之間是一一對應的,其關係是保存在一個全局的 Dictionary 裏。線程剛建立時並無 Run Loop,若是你不主動獲取,那它一直都不會有。Run Loop 的建立是發生在第一次獲取時,Run Loop 的銷燬是發生在線程結束時。你只能在一個線程的內部獲取其 Run Loop(主線程除外)。
在 CoreFoundation 裏面關於 RunLoop 有 5 個類:
• CFRunLoopRef
• CFRunLoopModeRef
• CFRunLoopSourceRef
• CFRunLoopTimerRef
• CFRunLoopObserverRef
其中 CFRunLoopModeRef 類並無對外暴露,只是經過 CFRunLoopRef 的接口進行了封裝。他們的關係以下:
對於上圖的理解:一個 Run Loop 包含若干個 Mode,每一個 Mode 又包含若干個 Source/Timer/Observer。每次調用 Run Loop 的主函數時,只能指定其中一個 Mode,這個 Mode 被稱做 CurrentMode。若是須要切換 Mode,只能退出 Loop,再從新指定一個 Mode 進入。這樣作主要是爲了分隔開不一樣組的 Source/Timer/Observer,讓其互不影響。
CFRunLoopSourceRef: 是事件產生的地方。Source 有兩個版本:Source0 和 Source1:
• Source0 只包含了一個回調(函數指針),它並不能主動觸發事件。使用時,你須要先調用 CFRunLoopSourceSignal(source)
,將這個 Source 標記爲待處理,而後手動調用 CFRunLoopWakeUp(runloop)
來喚醒 Run Loop,讓其處理這個事件。
• Source1 包含了一個 mach_port 和一個回調(函數指針),被用於經過內核和其餘線程相互發送消息。這種 Source 能主動喚醒 Run Loop 的線程,其原理在下面會講到。
CFRunLoopTimerRef: 是基於時間的觸發器,它和 NSTimer 是 Toll-Free Bridging 的,能夠混用。其包含一個時間長度和一個回調(函數指針)。當其加入到 Run Loop 時,Run Loop 會註冊對應的時間點,當時間點到時,Run Loop 會被喚醒以執行那個回調。
CFRunLoopObserverRef: 是觀察者,每一個 Observer 都包含了一個回調(函數指針),當 Run Loop 的狀態發生變化時,觀察者就能經過回調接受到這個變化。
Run Loop 對象處理的事件源分爲兩種:Input sources 和 Timer sources:
• Input sources:用分發異步事件,一般是用於其餘線程或程序的消息,好比:performSelector:onThread:...
• Timer sources:用分發同步事件,一般這些事件發生在特定時間或者重複的時間間隔上,好比:[NSTimer scheduledTimerWithTimeInterval:target:selector:...]
上面圖中展現了 Run Loop 的概念結構及各類事件源。其中 Input sources 分發異步事件給相應的處理程序而且調用 runUntilDate:
方法(這個方法會在該線程關聯的 NSRunLoop 對象上被調用)來退出其 Run Loop。Timer sources 分發事件到相應的處理程序,但不會引發 Run Loop 退出。
Input sources 有兩個不一樣的種類: Port-Based Sources 和 Custom Input Sources。Run Loop 自己並不關心 Input sources 是哪種類型。系統會實現兩種不一樣的 Input sources 供咱們使用。這兩種不一樣類型的 Input sources 的區別在於:Port-Based Sources 由內核自動發送,Custom Input Sources 須要從其餘線程手動發送。
Custom Input Sources
咱們可使用 Core Foundation 裏面的 CFRunLoopSourceRef 類型相關的函數來建立 Custom Input Sources。
Port-Based Sources
經過內置的端口相關的對象和函數,配置基於端口的 Input sources。(好比在主線程建立子線程時傳入一個 NSPort 對象,主線程和子線程就能夠進行通信。NSPort 對象會負責本身建立和配置 Input sources。)
Timer sources 在預設的時間點同步的傳遞消息,Timer 是線程通知本身作某件事的一種方式。
Foundation 中 NSTimer Class 提供了相關方法來設置 Timer sources。須要注意的是除了 scheduledTimerWithTimeInterval
開頭的方法建立的 Timer 都須要手動添加到當前 Run Loop 中。(scheduledTimerWithTimeInterval
建立的 Timer 會自動以 Default Mode 加載到當前 Run Loop中。)
Timer 在選擇使用一次後,在執行完成時,會從 Run Loop 中移除。選擇循環時,會一直保存在當前 Run Loop 中,直到調用 invalidated 方法。
Run Loop Mode 是指要被監聽的事件源(包括 Input sources 和 Timer sources)的集合 + 要被通知的 run-loop observers 的集合。每一次運行本身的 Run Loop 時,都須要顯示或者隱示的指定其運行於哪種 Mode。在設置 Run Loop Mode 後,你的 Run Loop 會自動過濾和其餘 Mode 相關的事件源,而只監視和當前設置 Mode 相關的源(通知相關的觀察者)。大多數時候,Run Loop 都是運行在系統定義的默認模式上。
首先咱們能夠看一下 App 啓動後 Run Loop 的狀態:
1 |
CFRunLoop { |
咱們能夠看到,系統默認註冊了 5 個 Mode:
下圖列出了 Cocoa 和 Core Foundation 中定義的一些 Modes:
CFRunLoopMode 和 CFRunLoop 的結構大體以下:
1 |
struct __CFRunLoopMode { |
這裏有個概念叫 「CommonModes」:一個 Mode 能夠將本身標記爲 「Common」 屬性(經過將其 Mode Name 添加到 RunLoop 的 「commonModes」 中)。每當 Run Loop 的內容發生變化時,Run Loop 都會自動將 _commonModeItems 裏的 Source/Observer/Timer 同步到具備 「Common」 標記的全部 Mode 裏。
應用場景舉例:主線程的 Run Loop 裏有兩個預置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經被標記爲 「Common」 屬性。DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。當你建立一個 Timer 並加到 DefaultMode 時,Timer 會獲得重複回調,但此時滑動一個 TableView 時,Run Loop 會將 mode 切換爲 TrackingRunLoopMode,這時 Timer 就不會被回調,而且也不會影響到滑動操做。
有時你須要一個 Timer,在兩個 Mode 中都能獲得回調,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 Run Loop 的 「commonModeItems」 中。」commonModeItems」 被 Run Loop 自動更新到全部具備 「Common」 屬性的 Mode 裏去。
你只能經過 Mode Name 來操做內部的 Mode,當你傳入一個新的 Mode Name 但 Run Loop 內部沒有對應 Mode 時,Run Loop會自動幫你建立對應的 CFRunLoopModeRef。對於一個 Run Loop 來講,其內部的 Mode 只能增長不能刪除。
蘋果公開提供的 Mode 有兩個:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你能夠用這兩個 Mode Name 來操做其對應的 Mode。
同時蘋果還提供了一個操做 Common 標記的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你能夠用這個字符串來操做 Common Items,或標記一個 Mode 爲 「Common」。使用時注意區分這個字符串和其餘 Mode Name。
對比上面說的事件源——它們是在特定的同步事件或異步事件發生時被觸發,Run Loop Observers 就不同了,它是在 Run Loop 執行本身的代碼到某一個指定位置時被觸發。咱們能夠用 Run Loop Observers 來跟蹤到這些事件:
與 Timer 相似,Run Loop Observers 也能夠只觀察一次或者反覆觀察。只觀察一次的話,就在 fire 後把本身從 Run Loop 中給移除掉就好了
當你爲一個須要長時間運行的線程配置 Run Loop 時,最好是能添加至少一個 Input source 到 Run Loop 中,這比用 Timer source 更好,Timer 要麼一次,觸發完了,就會結束,而以後 Run Loop 也就結束了,要麼循環,這樣就會致使週期性地喚醒線程,這其實是一種輪詢的形式。與之相反,Input source會一直等待對應的事件發生,而在事件發生前它能讓線程先休眠。
Run Loop 本質是一個處理事件源的循環。咱們對 Run Loop 的運行時具備控制權,若是當前沒有時間發生,Run Loop 會讓當前線程進入睡眠模式,來減輕 CPU 壓力。若是有事件發生,Run Loop 就處理事件並通知相關的 Observer。具體的順序以下:
因爲與 Timer source 和 Input source 相關的 observer 通知是在事件發生前發出去的,因此這些通知和真實的事件發生時間之間是存在必定的延時的。若是你須要精確的時間控制,而這個延時對你來講很致命的話,你可使用休眠通知和喚醒通知來校隊事件實際發生時間。
因爲 timer 和其餘一些週期性的事件是在你運行其對應的 Run Loop 的時候被分發的,因此當繞過這個 Loop 的時候,這些事件的分發也會被幹擾到。一個典型的例子就是當你實現一個鼠標事件追蹤的例程時,你進入到一個循環裏不斷地嚮應用請求事件,因爲你直接抓取這些事件而不是正常地由應用向你的例程分發,這時那些活動的timer也會沒法觸發,除非你的鼠標事件追蹤例程退出並將控制器交給應用。
能夠經過 Run Loop 對象來顯式地喚醒 Run Loop。其餘事件也能夠喚醒 Run Loop,好比:添加一個其餘的非基於端口的 Input source 能夠喚醒 Run Loop 當即處理這個 Input source,而不是等到其餘事件發生才處理。
使用 Core Foundation 中的方法一般是線程安全的,能夠被任意線程調用。若是修改了 Run Loop 的配置而後須要執行某些操做,咱們最好是在 Run Loop 所在的線程中執行這些操做。
使用 Foundation 中的 NSRunLoop 類來修改本身的 Run Loop,咱們必須在 Run Loop 的所在線程中完成這些操做。在其餘線程中給 Run Loop 添加事件源或者 Timer 會致使程序崩潰。
1 |
/// 用DefaultMode啓動 |
能夠看到,實際上 Run Loop 就是這樣一個函數,其內部是一個 do-while 循環。當你調用 CFRunLoopRun() 時,線程就會一直停留在這個循環裏;直到超時或被手動中止,該函數纔會返回。
從上面代碼能夠看到,Run Loop 的核心是基於 mach port 的,其進入休眠時調用的函數是 mach_msg()。爲了解釋這個邏輯,下面稍微介紹一下 OS X/iOS 的系統架構。
蘋果官方將整個系統大體劃分爲上述 4 個層次:
• 應用層包括用戶能接觸到的圖形應用,例如 Spotlight、Aqua、SpringBoard 等。
• 應用框架層即開發人員接觸到的 Cocoa 等框架。
• 核心框架層包括各類核心框架、OpenGL 等內容。
• Darwin 即操做系統的核心,包括系統內核、驅動、Shell 等內容,這一層是開源的,其全部源碼均可以在 opensource.apple.com 裏找到。
咱們在深刻看一下 Darwin 這個核心的架構:
其中,在硬件層上面的三個組成部分:Mach、BSD、IOKit (還包括一些上面沒標註的內容),共同組成了 XNU 內核。
XNU 內核的內環被稱做 Mach,其做爲一個微內核,僅提供了諸如處理器調度、IPC (進程間通訊)等很是少許的基礎服務。
BSD 層能夠看做圍繞 Mach 層的一個外環,其提供了諸如進程管理、文件系統和網絡等功能。
IOKit 層是爲設備驅動提供了一個面向對象(C++)的一個框架。
Mach 自己提供的 API 很是有限,並且蘋果也不鼓勵使用 Mach 的 API,可是這些 API 很是基礎,若是沒有這些 API 的話,其餘任何工做都沒法實施。在 Mach 中,全部的東西都是經過本身的對象實現的,進程、線程和虛擬內存都被稱爲」對象」。和其餘架構不一樣, Mach 的對象間不能直接調用,只能經過消息傳遞的方式實現對象間的通訊。」消息」是 Mach 中最基礎的概念,消息在兩個端口 (port) 之間傳遞,這就是 Mach 的 IPC (進程間通訊) 的核心。
一條 Mach 消息實際上就是一個二進制數據包 (BLOB),其頭部定義了當前端口 local_port 和目標端口 remote_port,發送和接受消息是經過同一個 API 進行的。
爲了實現消息的發送和接收,mach_msg() 函數其實是調用了一個 Mach 陷阱 (trap),即函數mach_msg_trap(),陷阱這個概念在 Mach 中等同於系統調用。當你在用戶態調用 mach_msg_trap() 時會觸發陷阱機制,切換到內核態;內核態中內核實現的 mach_msg() 函數會完成實際的工做,以下圖:
這些概念能夠參考維基百科: System_call、Trap_(computing))。
Run Loop 的核心就是一個 mach_msg()
,Run Loop 調用這個函數去接收消息,若是沒有別人發送 port 消息過來,內核會將線程置於等待狀態。例如你在模擬器裏跑起一個 iOS 的 App,而後在 App 靜止時點擊暫停,你會看到主線程調用棧是停留在 mach_msg_trap()
這個地方。
關於具體的如何利用 mach port 發送信息,能夠看看 NSHipster 這一篇文章,或者這裏的中文翻譯 。
關於Mach的歷史能夠看看這篇頗有趣的文章:Mac OS X 背後的故事(三)Mach 之父 Avie Tevanian。
在主線程執行的代碼,一般是寫在諸如事件回調、Timer 回調內的。這些回調會被 Run Loop 建立好的 AutoreleasePool 環繞着,因此不會出現內存泄漏,開發者也沒必要顯示建立 Pool 了。
蘋果註冊了一個 Source1 (基於 mach port 的) 用來接收系統事件,其回調函數爲 __IOHIDEventSystemClientQueueCallback()。
當一個硬件事件(觸摸/鎖屏/搖晃等)發生後,首先由 IOKit.framework 生成一個 IOHIDEvent 事件並由 SpringBoard 接收。這個過程的詳細狀況能夠參考這裏。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨後用 mach port 轉發給須要的 App 進程。隨後蘋果註冊的那個 Source1 就會觸發回調,並調用 _UIApplicationHandleEventQueue() 進行應用內部的分發。
_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理幷包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。一般事件好比 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。
當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨後系統將對應的 UIGestureRecognizer 標記爲待處理。
蘋果註冊了一個 Observer 監測 BeforeWaiting (Loop 即將進入休眠) 事件,這個 Observer 的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取全部剛被標記爲待處理的 GestureRecognizer,並執行 GestureRecognizer 的回調。
當有 UIGestureRecognizer 的變化(建立/銷燬/狀態改變)時,這個回調都會進行相應處理。
當在操做 UI 時,好比改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記爲待處理,並被提交到一個全局的容器去。
蘋果註冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調去執行一個很長的函數:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函數裏會遍歷全部待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 界面。
這個函數內部的調用棧大概是這樣的:
1 |
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv() |
NSTimer 其實就是 CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個 NSTimer 註冊到 Run Loop 後,Run Loop 會爲其重複的時間點註冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。Run Loop 爲了節省資源,並不會在很是準確的時間點回調這個 Timer。Timer 有個屬性叫作 Tolerance (寬容度),標示了當時間點到後,允許有多少最大偏差。
當調用 NSObject 的 performSelecter:afterDelay: 後,實際上其內部會建立一個 Timer 並添加到當前線程的 Run Loop 中。因此若是當前線程沒有 Run Loop,則這個方法會失效。
當調用 performSelector:onThread: 時,實際上其會建立一個 Timer 加到對應的線程去,一樣的,若是對應線程沒有 Run Loop 該方法也會失效。
GCD 提供的某些接口也用到了 Run Loop, 例如 dispatch_async()。
當調用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發送消息,RunLoop會被喚醒,並從消息中取得這個 block,並在回調 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
裏執行這個 block。但這個邏輯僅限於 dispatch 到主線程,dispatch 到其餘線程仍然是由 libDispatch 處理的。
一般使用 NSURLConnection 時,你會傳入一個 Delegate,當調用了 [connection start] 後,這個 Delegate 就會不停收到事件回調。實際上,start 這個函數的內部會會獲取 CurrentRunLoop,而後在其中的 DefaultMode 添加了4個 Source0 (即須要手動觸發的Source)。CFMultiplexerSource 是負責各類 Delegate 回調的,CFHTTPCookieStorage 是處理各類 Cookie 的。
NSURLConnectionLoader 中的 Run Loop 經過一些基於 mach port 的 Source 接收來自底層 CFSocket 的通知。當收到通知後,其會在合適的時機向 CFMultiplexerSource 等 Source0 發送通知,同時喚醒 Delegate 線程的 Run Loop 來讓其處理這些通知。CFMultiplexerSource 會在 Delegate 線程的 Run Loop 對 Delegate 執行實際的回調。