深入淺出 RunLoop (2) — 應用實踐

前言

接上篇 核心機制 ,本文主要介紹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

performSelector.jpg

上圖引自蘋果的官方文檔,除了輸入源和定時源之外,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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • 轉自http://blog.ibireme.com/2015/05/18/runloop 深入理解RunLoop ...
    飄金閱讀 999評論 0 4
  • 轉載:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling閱讀 1,456評論 0 13
  • 深入理解RunLoop 由ibireme| 2015-05-18 |iOS,技術 RunLoop 是 iOS 和 ...
    橙娃閱讀 879評論 1 2
  • 最近看了很多RunLoop的文章,看完很懵逼,決心整理一下,文章中大部分內容都是引用大神們的,但好歹對自己有個交代...
    小涼介閱讀 6,738評論 12 79
  • 臺州又出現了一個新的休閑公園,由四個次公園和一個主題公園組成一個大的中央公園。公園今年國慶期間剛剛開放,一直沒去看...
    柳枝冉冉閱讀 357評論 0 0