前言
接上篇 核心機制 ,本文主要介紹RunLoop在應用中的實踐。
iOS/OS X系統中很多基礎功能,比如自動釋放池就是由RunLoop實現或者協助實現的,所以RunLoop是iOS系統中基礎中的基礎,組件中的組件。
- 由RunLoop直接管理的機制有:自動釋放池、定時器、視圖刷新等機制。這些機制的生命周期完全由RunLoop管理。
- 需要RunLoop支持的機制有:事件響應、動畫、異步方法調用、網絡請求等機制。這些機制借助了RunLoop的特性完成自己的功能。
管理自動釋放池
自動釋放池,AutoreleasePool有兩種管理方式,一種方式是由程序員負責管理,通過AutoreleasePool塊將塊中的臨時變量在出塊的時候釋放掉,主要在循環讀取大文件中會用到。
另一種方式就是由系統管理AutoreleasePool的創建和銷毀,實質上這個系統管理就是Runloop管理的。
App啟動后,主線程RunLoop中會注冊Observer,分別在RunLoopEntry、BeforeWaiting和Exit時調用。回調都是_wrapRunLoopWithAutoreleasePoolHandler()。
RunLoopEntry Observer 在RunLoop進入的時候會被觸發,在_wrapRunLoopWithAutoreleasePoolHandler()函數中調用_objc_autoreleasePoolPush()創建自動釋放池。order是- 2^31,優先級最高,確保在所有回調之前創建自動釋放池。
RunLoopBeforWaiting Observer 在RunLoop進入休眠之前被觸發,同樣在_wrapRunLoopWithAutoreleasePoolHandler()函數中處理,但是調用的方法不同,分別調用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()方法。釋放舊的自動釋放池,同時創建新的自動釋放池。
RunLoopExit Observer 在RunLoop退出的時候被觸發。調用_objc_autoreleasePoolPop()釋放自動釋放池。
由于RunLoop管理AutoreleasePool,所以在線程中執行代碼,無論是事件回調,還是Timer回調都回被AutoreleasePool環繞,所以不會有內存泄露的問題。
管理定時器
timer是RunLoop的事件源之一,timer添加到RunLoop之后,RunLoop會在timer的時間點上注冊定時事件。因為各種原因,RunLoop執行回調的時間點并不準確,可能在執行一個長任務,可能在其他mode下。Timer有個屬性叫Tolerance,寬容度,這個屬性標示了當timer被觸發的時候同標定的時間點允許有多大的誤差度。
如果超過寬容度在這個時間點timer的回調函數不會被執行。同樣的如果某個時間點被錯過了,則這個時間點也會被跳過,回調函數不會被觸發執行。也就出現了,1秒執行一次的timer,理論上1分鐘應該執行60次,但是出現了執行57、58次的情況。
NSTimer同CFRunLoopTimerRef 是 toll-free bridged的。底層由XNU 內核的mk_timer 驅動。
管理視圖刷新
當視圖內容更新的時候,調用layoutSubView方法進行重新布局,調用drawRect方法進行重繪。我們都知道在開發的時候不能直接調用layoutSubviews或者drawRect方法,而是調用setNeedsLayout方法觸發重新布局,setNeedsDisplay方法觸發重新繪制。這樣做的目的是為了效率和流暢度,眾所周知界面的重新布局和重新繪制都是非常耗時的操作,如果在短時間內頻繁進行這個操作,CPU就沒辦法進行其他操作,影響app整體的運行效率和流暢度。所以將需要重排、重繪的View和Layer進行標記,在一次RunLoop循環中只進行一次重排、重繪操作。
這個視圖刷新的機制就需要RunLoop去支持。RunLoop Observer會在即將進入休眠 BeforeWaiting 和 退出 Exit 的時候調用CFRunLoopObservermPv()函數,這個函數會遍歷所有有標記的View和Layer,執行真正的重新布局和重新繪制方法。達到刷新視圖界面的目的。
CFRunLoopObservermPv()
QuartzCore::observer_callback:
CA::commit();
CA::commit_transaction();
layout_and_display_if_needed();
layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
display_if_needed();
[CALayer display];
[UIView drawRect];
支持事件響應
在RunLoop中事件源分為Source0和Source1。Source1事件是可以主動喚醒RunLoop的,Source1除了回調函數外還有一個mack_port端口,通過這個端口來接收系統事件,回調函數是_IOHIDEventSystemClientQueueCallback()。
當硬件事件如觸屏、搖晃、翻轉、鎖屏,系統會由IOKit產生一個用戶設備(human interface devices)事件。事件類型:IOHIDEvent。由SpringBoard組件負責接收。
SpringBoard 只接收按鍵,觸屏,加速,接近傳感器等4種 事件。之后通過mach_port發送給注冊的應用進程,應用進程通過Source1事件源響應這個事件,并通過_UIApplicationHandleEventQueue()進行分發。
_UIApplicationHandleEventQueue方法會將IOHIDEvent對象封裝成UIEvent對象再進行分發,手勢、屏幕旋轉交給Window處理,點擊事件交給響應者鏈處理。touchBegin、touchMove、touchEnd都是在這個方法中調用的。
手勢事件同touch事件是互斥的,如果UIEvent被識別成一個手勢,則不會當成touch事件來處理。系統會調用Cancel將touchBegin、touchMove中斷。
當_UIApplicationHandleEventQueue()識別一個手勢時,會將對應的手勢標記為待處理。當RunLoop 通過Observer 準備進入到休眠狀態時,Observer的回調函數會處理所有標記為待處理的手勢,并執行手勢的回調方法。
支持動畫渲染
Core Animation
Core Animation 在呈現的過程中有三個tree。
- model tree
- presentation tree
- render tree
model tree是我們可以直接操作的tree,當修改CALayer的時候,CALayer的屬性值會修改model tree。
presentation tree 是layer在屏幕中的真實位置也是一個CALayer對象。可以通過view.layer.presentationLayer 獲得。presentation是只讀的
render tree 是私有的,應用開發無法訪問到。render tree在專用的render server 進程中執行,是真正用來渲染動畫的地方,線程優先級高于主線程。所以即使app主線程阻塞,也不會影響到動畫的繪制工作。無論隱式還是顯式動畫都是在當前線程的RunLoop結束后提交到render tree。因為 commit transaction 操作是從app進程到render server 進程是IPC,會有進程間通訊開銷,所以官方不推薦我們手動 commit transaction。
CADisplayLink
CADisplayLink 的selector是在屏幕內容刷新完成的時候調用。實質上是向RunLoop注冊了一個Source0事件。CADisplayLink一般被用來執行自定義動畫和播放視頻,相比于CoreAnimation的方式,CADisplayLink會導致部分繪制工作放在了App的進程中進行,增大了CPU和內存的開銷,更容易引發性能問題。
CADisplayLink可以用來播放視頻,使用AVPlayerItemVideoOutput 提供一個樣板緩沖區(sample buffers),輸出到CAEAGLLayer 上。CAEAGLLayer 是 Core Animation Embedded Apple Graphics Library 的縮寫。OpenGL ES渲染出來的圖層在iOS中必須使用 CAEAGLLayer 。通過CADisplayLink 從緩沖區拿紋理內容,呈現在屏幕上。
官方代碼
支持異步方法調用
performSelector
上圖引自蘋果的官方文檔,除了輸入源和定時源之外,RunLoop還是performSelector的基礎設施。
我們使用 performSelector:onThread: 或者 performSelecter:afterDelay: 時,實際上系統會創建一個Timer并添加到當前線程的RunLoop中。所以如果當前線程沒有RunLoop,performSelector 方法就會失效。
GCD
dispatch_async() 方法,當第一個參數是主線程隊列的時候,libDispatch 會向主線程RunLoop發送mack_msg 消息。如果RunLoop在休眠態,會被喚醒,從消息中取得dispatch_async() 第二個參數 block 并執行。
為了確保GCD的有效性, dispatch_async() 到其他線程是由libDispatch處理,并不涉及到RunLoop。
支持網絡請求
在iOS中進行網絡通訊功能的開發一般都是基于NSURLConnection。NSURLConnection的底層是CFNetwork,CFNetwork是基于CFSocket的。NSURLConnection是基于socket的面向對象的網絡庫。
在iOS7之后蘋果提供了NSURLSession,相比NSURLConnection提供了更豐富的功能,如身份驗證、后臺下載等。底層都是基于CFNetwork和CFSocket。
NSURLConnection的start()方法中,會獲取當前的RunLoop,getCurrentRunLoop,然后在其中的defaultMode中添加Source0事件用于接收網絡回調。
NSURLConnection會創建兩個新線程:
- com.apple.CFSocket.private 線程,負責處理socket連接
- com.apple.NSURLConnectionLoader 線程, 用于接受底層socket 的 Source1 事件,通過 Source0 事件通知到NSURLConnection的start所在的RunLoop中。
參與整個網絡通訊的有3個線程,2個RunLoop。
線程 | RunLoop | 作用 |
---|---|---|
com.apple.CFSocket.private | 無 RunLoop | 處理socket連接 |
com.apple.NSURLConnectionLoader | 有RunLoop | 1、接收CFSocket的Source1通知。2、向應用線程的RunLoop的Source0發送通知 |
應用線程 | 有RunLoop | 通過Source0接收NSURLConnectionLoader 發送的通知,并回調delegate |
Run Loop應用實踐
Run Loop主要有以下三個應用場景:
- 維護線程的生命周期,讓線程不自動退出
- 創建常駐線程,執行一些會一直存在的任務。該線程的生命周期跟App相同
- 在一定時間內監聽某種事件,或執行某種任務的線程
維護線程的生命周期
isFinished為Yes時退出。
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) {
@autoreleasepool {
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
}
}
創建常駐線程
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
在一定時間內監聽某種事件
- 如下代碼,在30分鐘內,每隔30s執行onTimerFired:。這種場景一般會出現在,如我需要在應用啟動之后,在一定時間內持續更新某項數據。
@autoreleasepool {
NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:30
target:self
selector:@selector(onTimerFired:)
userInfo:nil
repeats:YES];
[runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]];
}
- AFNetworking中RunLoop的創建
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 這里主要是監聽某個 port,目的是讓RunLoop不會退出,確保該 Thread 不會被回收
[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 開發注意
//錯誤做法
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
while (!self.isCancelled && !self.isFinished) {
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
};
//正確做法
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) {
@autoreleasepool {
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
}
}
參考文章
http://iphonedevwiki.net/index.php/IOHIDFamily
https://en.wikipedia.org/wiki/SpringBoard
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
https://developer.apple.com/reference/foundation/urlsession
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html