在介紹runLoop之前有這樣幾個關鍵字需要理解:
-
source0
:不是基于端口的輸入源 -
source1
:用來接收系統事件,當一個硬件事件(觸摸/鎖屏/搖晃等)發生后,首先由IOKit.framework
生成一個IOHIDEvent
事件并由SpringBoard
接收,可參考官方文檔。SpringBoard
只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用mach port
轉發給需要的App進程。隨后蘋果注冊的那個source1
就會觸發回調,并調用_UIApplicationHandleEventQueue()
進行應用內部的分發。
_UIApplicationHandleEventQueue()
會把IOHIDEvent
處理并包裝成UIEvent
進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。通常事件比如 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。 -
mach port
:機械接口,iOS內核的一種通信機制 -
timer
:定時器 -
run loop mode
:包含 input sources 和 被監視的 timers 和 將要被通知的 run-loop 的observer 所組成的集合。在執行該運行循環的期間,只有與指定運行的 mode 相關聯的 source 才會被檢測和允許去發送他們的事件。 -
observer
:runLoop的觀察者 -
input source
:異步傳遞事件到你的線程中。event 的source 取決于 input source 的類型,通常是兩個類型中的一個(基于port的和自定義的)。 -
mac_msg()
:RunLoop 的核心就是一個 mach_msg() (見上面代碼的第7步),RunLoop 調用這個函數去接收消息,如果沒有別人發送 port 消息過來,內核會將線程置于等待狀態。例如你在模擬器里跑起一個 iOS 的 App,然后在 App 靜止時點擊暫停,你會看到主線程調用棧是停留在 mach_msg_trap() 這個地方。
runLoop
:消息機制的處理模式,消息循環(事件循環)
作用:在有事件的時候當前的NSRunLoop的線程處理事件,沒有事件的時候線程進行休眠
NSRunLoop就是一直在循環檢測,從線程start到線程end,檢測input source(如點擊,雙擊等操作)同步事件,檢測time source同步事件,檢測到輸入源會執行處理函數,首先會產生通知,corefunction向線程添加runloop observers來監聽事件,意在監聽事件發生時來做處理。
在單線程的App中,不需要注意Run Loop,但不代表沒有。程序啟動時,系統已經在主線程中加入Run Loop。它保證了我們的主線程在運行起來后,就處于一種“等待”的狀態(而不像一些命令行程序在運行一次就結束了),這個時候如果有接收到的時間(Timer的定時到了或是其他線程的消息),就會執行任務,否則就處于休眠狀態。
runloopmode是一個集合,包括監聽:事件源,定時器,以及需通知的runloop observers
模式包括:
- default模式:幾乎包括所有輸入源(除NSConnection)NSDefaultRunLoopMode模式
- mode模式:處理modal panels
- connection模式:處理NSConnection事件,屬于系統內部,用戶基本不用
- event tracking模式:如組件拖動輸入源 UITrackingRunLoopModes不處理定時事件
- common modes模式:NSRunLoopCommonModes這是一組可配置的通用模式。將input source與該模式關聯則同時也將input source與改組中的其它模式進行了關聯
每次運行一個run loop,你指定(顯式或隱式)run loop的運行模式。當相應的模式傳遞給run loop時,只有與該模式對應的input sources才被監控并允許run loop對事件進行處理(與此類似,也只有與該模式對應的observers才會被通知)
例:
1.在timer與table同時執行情況,當拖動table時,runloop進入UITrackingRunLoopModes模式下,不會處理定時事件,此時timer不能處理,所以此時將timer加入到NSRunLoopCommondModes模式(addTimer forMode)
2.在scroll一個頁面時來松開,此時connection不會收到消息,由于scroll時runloop為UITrackingRunLoopModes模式,不接收輸入源,此時要修改connection的mode
[scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
關于-(BOOL)runMode:(NSString *)mode beforeDate:(NSDate)date;方法
指定
為什么要用runLoop:
- 使程序一直運行接受用戶輸入
- 決定程序在何時應該處理哪些Event
- 調用解耦(對于編程經驗為0的完全沒搞懂這個意思,解釋為Message Queue)
- 節省CPU時間
蘋果用 RunLoop 實現的功能
- AutoreleasePool
- 事件響應
- 手勢識別
- 界面更新
- 定時器
- PerformSelecter
- 關于GCD
- 關于網絡請求
CFRunLoopRef是基于pthread來管理的
RunLoop的底層實現
蘋果官方將整個系統大致劃分為上述4個層次:
1.應用層包括用戶能接觸到的圖形應用,例如 Spotlight、Aqua、SpringBoard 等。
2.應用框架層即開發人員接觸到的 Cocoa 等框架。
3.核心框架層包括各種核心框架、OpenGL 等內容。
4.Darwin 即操作系統的核心,包括系統內核、驅動、Shell 等內容,這一層是開源的,其所有源碼都可以在 opensource.apple.com 里找到。
AutoreleasePool
App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 創建自動釋放池。其 order 是-2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。
第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創建新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其他所有回調之后。
在主線程執行的代碼,通常是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 創建好的 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 界面。
這個函數內部的調用棧大概是這樣的:
_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 注冊到 RunLoop 后,RunLoop 會為其重復的時間點注冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop為了節省資源,并不會在非常準確的時間點回調這個Timer。Timer 有個屬性叫做 Tolerance (寬容度),標示了當時間點到后,容許有多少最大誤差。
如果某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回調也會跳過去,不會延后執行。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。
CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現原理更復雜,和 NSTimer 并不一樣,其內部實際是操作了一個 Source)。如果在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 相似),造成界面卡頓的感覺。在快速滑動TableView時,即使一幀的卡頓也會讓用戶有所察覺。Facebook 開源的 AsyncDisplayLink 就是為了解決界面卡頓的問題,其內部也用到了 RunLoop,這個稍后我會再單獨寫一頁博客來分析。
PerformSelecter
當調用 NSObject 的 performSelecter:afterDelay: 后,實際上其內部會創建一個 Timer 并添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。
當調用 performSelector:onThread: 時,實際上其會創建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。
關于GCD
實際上 RunLoop 底層也會用到 GCD 的東西,比如 RunLoop 是用 dispatch_source_t 實現的 Timer。但同時 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。
當調用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發送消息,RunLoop會被喚醒,并從消息中取得這個 block,并在回調 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里執行這個 block。但這個邏輯僅限于 dispatch 到主線程,dispatch 到其他線程仍然是由 libDispatch 處理的。
關于網絡請求
iOS 中,關于網絡請求的接口自下至上有如下幾層:
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire
?CFSocket 是最底層的接口,只負責 socket 通信。
?CFNetwork 是基于 CFSocket 等接口的上層封裝,ASIHttpRequest 工作于這一層。
?NSURLConnection 是基于 CFNetwork 的更高層的封裝,提供面向對象的接口,AFNetworking 工作于這一層。
?NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程),AFNetworking2 和 Alamofire 工作于這一層。
下面主要介紹下 NSURLConnection 的工作過程。
通常使用 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 執行實際的回調。
RunLoop 的實際應用舉例
AFNetworking
AFURLConnectionOperation 這個類是基于 NSURLConnection 構建的,其希望能在后臺線程接收 Delegate 回調。為此 AFNetworking 單獨創建了一個線程,并在這個線程中啟動了一個 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 啟動前內部必須要有至少一個 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先創建了一個新的 NSMachPort 添加進去了。通常情況下,調用者需要持有這個 NSMachPort (mach_port) 并在外部線程通過這個 port 發送消息到 loop 內;但此處添加 port 只是為了讓 RunLoop 不至于退出,并沒有用于實際的發送消息。
- (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];
}
當需要這個后臺線程執行任務時,AFNetworking 通過調用 [NSObject performSelector:onThread:..] 將這個任務扔到了后臺線程的 RunLoop 中。
AsyncDisplayKit
AsyncDisplayKit 是 Facebook 推出的用于保持界面流暢性的框架,其原理大致如下:
UI 線程中一旦出現繁重的任務就會導致界面卡頓,這類任務通常分為3類:排版,繪制,UI對象操作。
排版通常包括計算視圖大小、計算文本高度、重新計算子式圖的排版等操作。
繪制一般有文本繪制 (例如 CoreText)、圖片繪制 (例如預先解壓)、元素繪制 (Quartz)等操作。
UI對象操作通常包括 UIView/CALayer 等 UI 對象的創建、設置屬性和銷毀。
其中前兩類操作可以通過各種方法扔到后臺線程執行,而最后一類操作只能在主線程完成,并且有時后面的操作需要依賴前面操作的結果 (例如TextView創建時可能需要提前計算出文本的大小)。ASDK 所做的,就是盡量將能放入后臺的任務放入后臺,不能的則盡量推遲 (例如視圖的創建、屬性的調整)。
為此,ASDK 創建了一個名為 ASDisplayNode 的對象,并在內部封裝了 UIView/CALayer,它具有和 UIView/CALayer 相似的屬性,例如 frame、backgroundColor等。所有這些屬性都可以在后臺線程更改,開發者可以只通過 Node 來操作其內部的 UIView/CALayer,這樣就可以將排版和繪制放入了后臺線程。但是無論怎么操作,這些屬性總需要在某個時刻同步到主線程的 UIView/CALayer 去。
ASDK 仿照 QuartzCore/UIKit 框架的模式,實現了一套類似的界面更新的機制:即在主線程的 RunLoop 中添加一個 Observer,監聽了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回調時,遍歷所有之前放入隊列的待處理的任務,然后一一執行。
需要runloop的時候
當你在子線程中有一下場景的時候需要開啟你的run loop
你需要在以下這些場景中開啟你的 run loop:
- 使用 port 或者自定義 input source 來和其他線程進行通信。
- 在線程中使用 timer 。
- 在 Cocoa 的應用中使用任何與 performSelector…相關的方法。
- 讓你的線程繼續執行周期性的任務。