runLoop,正如其名,表示一直運行著的循環。
一般來說,一個線程只能執行一個任務,執行完就會推出,如果我們需要一種機制,讓線程能隨時處理時間但并不退出,而runLoop就是這樣一個機制;而這種機制的關鍵在于:如何管理消息/消息,如何讓線程在沒有處理消息的時候休眠以避免資源占用,在有消息的時刻立刻被喚醒。
所以,runLoop實際上就是一個對象,這個對象管理了其需要處理的時間和消息,并提供了一個入口函數來執行上面的邏輯。線程執行了這個函數之后,就會一直處于“接收消息-等待-處理”的循環中,知道這個循環結束,函數返回。
ios提供了兩個這樣的對象:NSRunLoop和CFRunLoopRef。
一、線程與runLoop
① 直線線程:該線程執行的任務是一條直線;
② 圓形線程:該線程是一個圓,不斷循環,知道通過某種方式截止,ios中,圓形線程就是通過runLoop實現的。
① runLoop和線程緊密相連,可以說,runLoop是為了線程而生的,沒有線程,就沒有runLoop存在的必要;
② 每個線程都有其對應的runLoop對象;
③ 主線程的runLoop是默認啟動的,而其他線程的runLoop是默認沒有啟動的;
二、RunLoop輸入事件來源
runLoop接收的輸入事件來自兩種來源:輸入源和定時源;
1.輸入源
傳遞異步事件,通常消息來自其他線程或程序。輸入源傳遞異步消息給相應的處理程序,并調用runUntilDate方法來退出;
當你創建輸入源,需要將其分配給runLoop的一個或多個模式,模式只會在特定事件影響監聽的源。
以下是輸入源的類型:
① 基于端口的輸入源:基于端口的輸入源是有內核自動發送;
cocoa和Core Foundation內置支持使用端口相關的對象和函數來創建基于端口的源。
例如:在Core Foundation中,使用端口相關的函數來創建端口和runLoop源;
② 自定義輸入源:自定義源需要人工從其他線程發送。
Core Foundation中可以使用CFRunLoopSourceRef等來創建源,也可以使用回調函數來配置源。Core Foundation會在配置源的不同地方調用回調函數,處理輸入事件,在源從runLoop移除的時候清理它;
③ Cocoa上的selector源
定時源在預設的時間點同步傳遞消息,這些消息都會在特定事件或者重復的時間間隔,定時源則傳遞消息個處理線程,不會立即退出runLoop。
定時器并不是實時機制,定時器和你的runLoop的特定模式相關,如果定時器所在的模式當前未被runLoop監視,那么定時器將不會開始,知道runLoop運行在響應的模式下。
三、RunLoop的相關知識點
runLoop中使用mode來指定時間在運行循環中的優先級,分為:
① NSDefaultRunLoopMode(kCFRunLoopDefaultMode): 默認,空閑狀態;
② UITrackingRunLoopMode:scrollView滑動時;
③ UIInitializationRunLoopMode: 啟動時;
④ NSRunLoopCommonModes(kCFRunLoopCommonModes):mode集合。
ps:其中①和④是蘋果公開的mode。
源是在合適的同步或異步事件發生時觸發,而runLoop觀察者則是在runLoop本身運行的特定時候觸發,你可以使用runLoop觀察者為處理某一特定事件或是進入休眠的程序做準備??梢詫unLoop觀察者和以下事件關聯:
① runLoop入口
② runLoop何時處理一個定時器;
③ runLoop何時處理一個輸入源;
④ runLoop何時進入睡眠狀態;
⑤ runLoop何時被喚醒,但在喚醒之前要處理的事件;
⑥ runLoop終止;
在創建的時候,也可以指定runLoop觀察者可以只用一次或者循環使用,若只用一次,那么它在啟動后,會把自己從runLoop中移除,而循環的觀察者不會。
每次運行runLoop,線程的runLoop會自動處理之前未處理的消息,并通知相關的觀察者,具體的順序如下:
① 通知觀察者runLoop已經啟動;
② 通知觀察者任何即將要開始的定時器;
③ 通知觀察著任何即將啟動的非基于端口的源;
④ 啟動任何準備好的非基于端口的源;
⑤ 如果基于端口的源準備好并處于等待狀態,立即啟動,并進入步驟⑨;
⑥ 通著觀察者線程進入休眠;
⑦ 將線程至于休眠知道任意下面的事件發生:
A.某一時間到達基于端口的源;
B.定時器啟動;
C.runLoop設置的時間已經超過;
D.runLoop被顯示喚醒;
⑧ 通知觀察者線程將被喚醒;
⑨ 處理未處理的事件
A.如果用戶定義的定時器啟動,處理定時器并重啟runLoop,進入步驟2
B.如果輸入源啟動,傳遞響應的消息;
C.如果runLoop被顯示喚醒,而且時間還沒超過,重啟runLoop,進入步驟2
⑩ 通知觀察者runLoop結束。
ps:
① 如果事件到達,消息會被傳遞給響應的處理程序來處理,runLoop處理完當次事件后,runLoop就會推出,而不管之前預定的時間到了沒有。
② 可以重新啟動runLoop來等待下一事件;
③ 如果線程中有需要處理的源,但是響應的事件沒有到來的時候,線程就會休眠等待相應事件的發生。
僅當在為你的程序創建輔助線程的時候,你才需要顯示運行一個runLoop
對于輔助線程,你需要判斷一個runLoop是否是必須的。如果是必須的,那么你要自己配置并啟動它,你不需要再任何情況下都去啟動一個線程的runLoop。runLoop在你要和線程有更多的交互時才需要,比如以下情況:
① 使用端口或者自定義輸入源來和其他線程通信;
② 使用線程的定時器;
③ Cocoa中使用任何performSelector的方法;
④ 使線程周期性工作。
四、CFRunLoop介紹
CoreFoundation中有5個關于runLoop的類:
① CFRunLoopRef:
② CFRunLoopModeRef:該類并沒有對外暴露;
③ CFRunLoopSourceRef:時間產生的地方,source有兩個版本:
A.source0:只包含一個回調,它并不能主動觸發事件,使用時,需先調用CFRunLoopSourceSignal將這個source標記為待處理,后調用CFRunLoopWakeUp來喚醒runLoop,讓其處理這個事件;
B.source1:包含一個mach_port和一個回調,被用于通過內核和其他線程相互發送消息,這種source能主動喚醒runLoop線程。
④ CFRunLoopTimerRef:基于事件的觸發器,他和NSTimer是可以混用的,其包含一個事件長度和回調,當其加入到runLoop中是,runLoop會注冊對應的時間點,當時間點到時,runLoop會被喚醒以執行那個回調;
⑤ CFRunLoopObserverRef:觀察者,包含了一個回調,當runLoop的狀態發生變化時,觀察者就能通過回調接收到變化,可觀測的時間點有:
kCFRunLoopEntry? ? ? ? ? ? ? ? ? ? //即將進入Loop
kCFRunLoopBeforeTimers? ? ? ? ? ? ? //即將處理Timer
kCFRunLoopBeforeSources? ? ? ? ? ? //即將處理Source
kCFRunLoopBeforeWaiting? ? ? ? ? ? //即將進入休眠
kCFRunLoopAfterWaiting? ? ? ? ? ? ? //剛從休眠中喚醒
kCFRunLoopExit? ? ? ? ? ? ? ? ? ? ? //即將退出Loop
ps:
① 一個runLoop包含若干個Mode,每個Mode包含若干個Source/Timer/Observer;
② 每次調用runLoop的主函數,只能指定其中一個Mode,如果需要切換Mode,只能退出Loop,再重新指定一個Mode進入;
③ Source/Timer/Observer統稱為mode item,一個item可以同時加入多個mode,但一個item被重復加入同一個mode是沒效果的;
④ 如果一個mode鐘一個item都沒有,runLoop就會退出,不進入循環。
CFRunLoop的結構如下
struct __CFRunLoop {
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
...
};
CFRunLoopMode的結構如下:
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...
};
其中,CFRunLoop對外暴露的管理Mode的接口有兩個:
CFRunLoopAddCommonMode
CFRunLoopRunInMode
Mode暴露的管理mode item的接口有下面幾個:
CFRunLoopAddSource
CFRunLoopAddObserver
CFRunLoopAddTimer
CFRunLoopRemoveSource
CFRunLoopRemoveObserver
CFRunLoopRemoveTimer
ps:
① 只能通過mode name來操作內部的mode,當你傳入一個新的mode name但runLoop內部沒有對應的Mode時,runLoop會自動幫你創建對應的CFRunlLoopModeRef。對于一個runLoop來說,其內部的mode只能增加,而不能刪除。
② commonModes:一個mode可以將自己標記為“Common”蘇醒,每當runLoop的內容發生變化時,runLoop都會自動將_commonModeItems里的item同步到具有“Common”標記的所有Mode里。
實際上,runLoop就是一個函數,其內部是一個do-while循環,當你調用CFRunLoop()時,線程就會一直停留在這個循環中;直到超時或被手動停止,該函數才會返回。
五、runLoop的底層實現
runLoop的核心是基于mach port的,其進入休眠時調用的函數是mach_msg(),為了解釋這個邏輯,需要介紹下ios的系統框架;
蘋果官方將系統大致劃分為4個層次:
① 應用層:包括用戶能解除到的圖層應用,例如Spotlight、Aqua、SpringBoard等
② 應用框架層:即開發人員解除到的Cocoa等框架;
③ 核心框架層:包括各種核心框架、OpenGL等內容;
④ Darwin:即操作系統的核心,包括系統內核、驅動、Shell等內容,這層是開源的
其中,在硬件層上面的三個組成部分:Mach,BSD,IOKit,共同組成了XNU內核。
① Mach:XNU內核的內環被稱作Mach,其作為一個為內核,僅提供了諸如處理器調度、IPC(進程間通信)等非常少量的基礎服務;
② BSD:BSD層可以看做圍繞Mach層的一個外環,提供了諸如進程管理、未見系統和網絡等功能;
③ IOKit:該層為設備驅動提供了一個面向對象(C++)的框架。
Mach本身提供的API非常有限,而且蘋果也不鼓勵使用Mach的API,但是這些API非?;A,如果沒有這些API的話,其他任何工作都無法實施。在Mach中,所有的東西都是通過自己的對象實現的,進程、線程和虛擬內存都被稱為“對象”。和其他架構不公,Mach對象間不能直接調用,只能通過消息傳遞的方式實現對象間的通信?!跋ⅰ笔荕ach中最基礎的概念,消息在兩個端口(port)之間傳遞,就是Mach的IPC的核心。
為了實現消息的發送和接收,mach_msg()函數實際上是調用了一個Mach陷阱(trap)即函數mach_msg_trap(),陷阱這個概念在Mach中等同于系統調用。當你在用戶狀態調用mach_msg_trap()時會觸發陷阱機制,切換到內核態;內核態中內核實現mach_msg()函數會完成實際的工作。
ps:runLoop的核心就是一個mach_msg(),runLoop調用這個函數去接收消息,如果沒有別人發送port消息過來,內核會將線程置于等待撞他。
例如你在模擬器里跑起一個app,然后在app靜止時點擊暫停,你會看到主線程調用棧是停留在mach_msg_trap()這個地方。
六、蘋果用runLoop實現的功能
app啟動后,蘋果在主線程的runLoop里注冊了兩個Observer:
① 第一個Observer監視事件是Entry(即將進入Loop),其回調內會創建自動釋放池,優先級最高,保證釋放池發生在所有回調之前;
② 第二個Observer監視了兩個事件,BeforeWaiting(準備進入休眠)時釋放舊的池并創建新的池;Exit(即將推出loop)時釋放自動釋放池;優先級最低,保證其釋放池發生在其他回調之后。
蘋果注冊了一個Source1(基于mach port)用來接收系統事件
當一個硬件事件(觸摸/搖晃等)發生后,首先由IOKit.framework生成一個IOHIDEvent
事件,并由SpringBoard接收,隨后用mach port轉發給需要的App進程。隨后蘋果注冊
的那個Source1會觸發回調,并調用方法UIApplicationHandleEventQueue()進行應用內
部的分發,
UIApplicationHandleEventQueue()方法會把IOHIDEvent處理并包裝成UIEvent分發,
通常事件比如UIButton點擊,touch事件都是在這個回調中完成的。
當上邊的UIApplicationHandleEventQueue()識別了手勢后,首先會打斷當前的touch系統回調,隨后系統將手勢標記為待處理。蘋果注冊了一個Observer檢測BeforeWaiting,在這個事件的回調函數中,獲取所有剛被標記為待處理的手勢,并執行手勢的回調。
當操作UI時,比如改變了Frame等,這個UIView/CALayer會被標記為待處理,并提交到一個全局的容器中。
蘋果注冊了一個Observer監聽BeforeWaiting和Exit,在回調用,會遍歷所有待處理的UIView/CALayer以執行實際的繪制和調整,并更新UI界面。
一個NSTimer注冊到RunLoop后,runLoop會為其重復的時間點注冊號時間,runLoop為了節省資源,并不會再非常準確的時間點回調這個timer。timer有個屬性叫做tolerance(寬容度),表示當時間點后,容許有多少最大誤差。
當調用NSObject的PerformSelector方法后,實際上其內部會創建一個timer并添加到當前線程的runLoop中,如果當前線程沒有runLoop,則這個方法會失效。
實際上runLoop底層,也用到了GCD的東西,比如runLoop用dispatch_source_t實現
的timer,但同時GCD提供的某些方法也用到了runLoop,例如dispatch_async()。
關于網絡請求的接口,自上之下有如下基層:
① CFSocket:是最底層的接口,只負責socket的通信;
② CFNetwork:是基于CFSocket等接口的上層封裝,ASIHttpRequest工作與這層;
③ NSURLConnection:是基于CFNetwork的更高層的封裝,提供面向對象的接口,
AFNetworking工作于這一層;
④ NSURLSession:是ios7中新增的接口,表面上和NSURLConnection并列,但底層
仍然用到了NSURLConnection的部分功能,AFNetworking2和Alamofire工作于這層。
通常使用 NSURLConnection 時,你會傳入一個 Delegate,當調用了 [connection start] 后,這個 Delegate 就會不停收到事件回調。實際上,start 這個函數的內部會獲取CurrentRunLoop,然后在其中的 DefaultMode 添加了4個 Source0 (即需要手動觸發的Source) 。CFMultiplexerSource 是負責各種 Delegate 回調的,CFHTTPCookieStorage 是處理各種 Cookie 的。
當開始網絡傳輸時,我們可以看到 NSURLConnection 創建了兩個新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 線程是處理底層 socket 連接的。NSURLConnectionLoader 這個線程內部會使用 RunLoop 來接收底層 socket 的事件,并通過之前添加的 Source0 通知到上層的 Delegate。
NSURLConnectionLoader 中的 RunLoop 通過一些基于 mach port 的 Source 接收來自底層 CFSocket 的通知。當收到通知后,其會在合適的時機向 CFMultiplexerSource 等 Source0 發送通知,同時喚醒 Delegate 線程的 RunLoop 來讓其處理這些通知。
CFMultiplexerSource 會在 Delegate 線程的 RunLoop 對 Delegate 執行實際的回調。