RunLoop 從理解到應用

2021.2 @Hanniyahtml

最近因準備面試,有較多學習內容。計劃產出的是有較多我我的理解和知識結構的幾篇學習內容:RunLoop、Runtime、AutoreleasePool,本篇是 RunLoop 相關,歡迎各位做爲查缺補漏來閱讀~ios

面試思路大綱

是什麼web

  • 是在一個【線程】中,持續調度各類任務的運行循環機制【本質:while循環】

作什麼面試

  1. performTask() 執行任務:Block、Source0、Source一、Main queue、Timer
  2. callout_to_observer() 通知外部:Activity、Source0、Timer
  3. sleep() 睡眠

應用:Timer、線程保活、卡頓檢測數組

1. RunLoop 簡介

1.1 做用

  • 保持程序持續運行:

程序一啓動,在 UIApplicationMain 就會開一個主線程,跑一個和主線程對應的 RunLoop,這個 RunLoop 保證主線程不會被銷燬,也就保證了程序的持續運行。安全

  • 處理App中的各類事件,如觸摸事件、定時器事件、Selector事件等
  • 節省CPU資源,提升程序性能

當沒任務時,RunLoop會告訴CPU要去休息,這時CPU就會將其資源釋放出來去作其餘的事情,當有事情作的時候RunLoop就會去作事markdown

1.2 特色

  • 與線程的關係

線程和 RunLoop 之間一一對應,其對應關係保存在一個全局的 Dictionary 裏,線程是 key,RunLoop 是 value。app

  • 生命週期

子線程的 RunLoop 的建立發生在第一次獲取時(若建立子線程後不主動獲取,則不會建立,能夠理解爲懶加載),RunLoop 的銷燬發生在線程結束時。框架

  • 獲取

只能在一個線程的內部獲取其 RunLoop(主線程除外)。函數

//Foundation
[NSRunLoop currentRunLoop]; // 得到當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 得到主線程的RunLoop對象

//Core Foundation
CFRunLoopGetCurrent(); // 得到當前線程的RunLoop對象
CFRunLoopGetMain(); // 得到主線程的RunLoop對象
複製代碼

NSRunLoop 是對 CFRunLoopRef 的一層封裝
CFRunLoopRef 的 API 是線程安全的;NSRunLoop 提供了面向對象的 API,但這些 API 不是線程安全的。

開一個子線程時建立 RunLoop,不是經過 alloc init 方法建立,而是直接經過調用 currentRunLoop 方法來建立,由於它自己是一個懶加載。

2. RunLoop 作什麼

2.1 performTask() 執行任務

DoBlocks()

  • 開發者可以使用

DoSources0()

  • 開發者可以使用
  • Source 0 不能主動喚醒 RunLoop

DoSources1()

  • 只能系統使用
  • Source 1 可以主動喚醒 RunLoop
  • 基於 mach_msg 函數,經過讀取 port 上內核消息隊列的消息來決定執行的任務。
  • 任務包括渲染 UI 等

DoMainQueue()

  • 開發者可以使用,調用 GCD 的 API 將任務放入到 main queue 中

DoTimers()

  • 開發者可以使用,調用 NSTimer 的 API 便可註冊被執行的任務

2.2 callout_to_observer() 通知外部

DoObservers-Activity 當前狀態

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),        //每次進入Runloop(如切換mode後)
    kCFRunLoopBeforeTimers = (1UL << 1), //即將DoTimers
    kCFRunLoopBeforeSources = (1UL << 2),//即將DoSources
    kCFRunLoopBeforeWaiting = (1UL << 5),//當前線程即將進入睡眠(若當前隊列無多餘消息則進入睡眠)
    kCFRunLoopAfterWaiting = (1UL << 6), //當前線程從睡眠中恢復(讀出隊列消息,繼續執行)
    kCFRunLoopExit = (1UL << 7),         //退出Runloop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼

DoObservers-Timer

表示將要處理Timer

DoObservers-Source0

表示將要處理Source0

2.3 sleep() 睡眠

2.4 綜合流程圖

Runloop 的每次 loop 不老是按順序執行上面的各類 performTaskcallout_to_observer,而是糅合在一塊兒各類跳轉

借用mrpeak的圖來理解完整的流程: rl00.png

  • Poll:若是處理了source0任務,poll值爲true,睡眠先後不會進行通知。
  • DoBlocks -> DoSource0 -> (睡眠) -> DoSource1/DoMainQueue/DoTimers -> DoBlocks 循環

睡眠喚醒 RunLoop 後 DoSource1/DoMainQueue/DoTimers 只會三選一

3. RunLoop 原理

3.1 本質:結構體

RunLoop 結構體

struct __CFRunLoop {
    ...//省略非核心成員
	  CFMutableSetRef _commonModes; 
    CFMutableSetRef _commonModeItems; 
    CFRunLoopModeRef _currentMode; //指向_CFRunLoopMode結構體的指針
    CFMutableSetRef _modes; //多個mode數組
};
複製代碼

Mode結構體

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
    ... //省略非核心成員
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
};
複製代碼
  • 一個RunLoop包含多個Mode,每一個Mode又包含多個Source/Timer/Observer

mainQueue 任務的執行和 mode 無關,mode 內沒有相關信息

3.2 Mode:CFRunLoopModeRef

Mode 分爲 Common Mode 和 Private Mode,因此 observer 並不會監控到全部 Runloop 的動態

  • RunLoop 的 Mode 切換

RunLoop啓動時選擇其中一個Mode做爲currentMode;
須要切換Mode時,只能退出RunLoop,再從新指定一個Mode進入,這樣作主要是爲了分隔開不一樣組的Source、Timer、Observer,讓其互不影響
若當前mode內沒有任何Source/Timer/Observer,RunLoop不會空轉,會馬上退出。

3.3 Source(0/1)/Timer/Observer

  • Source 事件產生
    • Source0:包含一個函數指針(回調),接受外界觸發的事件,不能主動喚醒RunLoop,只能經過Wakeup接口喚醒RunLoop來處理事件(觸摸事件、performSelectors)
    • Source1:包含一個mach_port和一個函數指針(回調),能主動喚醒RunLoop(基於Port的線程間通訊)
  • Timer:定時器,包含一個時間長度和一個函數指針(回調)
  • Observer:觀察者,包含一個函數指針(回調),經過回調監聽RunLoop的狀態

3.4 RunLoop 的內存管理

即將進入 RunLoop 時,經過 observer 觀察到 kCFRunLoopEntry 狀態,主線程 RunLoop 會建立一個 AutoreleasePool。

4. 面試題

4.1 不作處理時當拖動 tableview 時 NSTimer 會響應嗎?怎麼解決

不會響應。

緣由:NSTimer 默認只會調度到 kCFRunLoopDefaultMode,當 scrollView 滑動的時候,runloop 會進入 UITrackingRunLoopMode,那麼在 doTimer 的時候天然就不會觸發 NSTimer 的任務了

解決辦法:

  1. 將 NSTimer 也加入到 UITrackingRunLoopMode(但這樣timer被添加了兩次,不是同一個timer)
  2. 把 NSTimer 加入到 NSRunLoopCommonModes 裏,至關於將本身標記爲Common,全部也標記爲common的mode都會繼續處理這個事件。

但即便這樣,當 RunLoop 使用系統 private mode 時,也會存在不執行 Timer 的問題。

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
複製代碼

4.2 NSTimer 和 GCD 哪一個更精準?爲何

CGD 定時器更精準。由於

  1. NSTimer 是每次 Runloop 檢查一次到沒到時間,有偏差。

RunLoop爲了節省資源,並不會在很是準確的時間點回調這個Timer。Timer 有個屬性叫作 Tolerance (寬容度),標示了當時間點到後,允許有多少最大偏差。
若是某個時間點被錯過了,例如執行了一個很長的任務且也過了Timer的寬容度,則那個時間點的回調也會跳過去,不會延後執行。

  1. NSTimer 有可能由於 Mode 問題被延遲處理。
//建立隊列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    //1.建立一個GCD定時器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 須要對timer進行強引用,保證其不會被釋放掉,纔會按時調用block塊
    // 局部變量,讓指針強引用
    self.timer = timer;
    //2.設置定時器的開始時間,間隔時間,精準度
    //精準度 通常爲0 在容許範圍內增長偏差可提升程序的性能
    //GCD的單位是納秒 因此要 * NSEC_PER_SEC
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    //3.設置定時器要執行的事情
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"---%@--", [NSThread currentThread]);
    });
    dispatch_resume(timer); // 啓動
複製代碼

4.3 RunLoop 如何響應用戶事件、手勢、界面刷新

  • UIEvent 事件歷程:

手指觸摸屏幕

  1. IOKit.framework 封裝事件爲 IOHIDEvent 對象
  2. 端口通訊:經過 mach port 轉發到 APP,主線程 Runloop 中 Source1接收
  3. Runloop 進行回調(Source1回調 -> Source0)
  4. Source0 的回調將觸摸事件添加到事件隊列(FIFO)
  5. 出隊列時 UIApplication 開始尋找最佳響應者(Hit-testing)
  6. 事件被髮送至最佳響應者,進行響應或傳遞
  • 手勢:

系統註冊了一個 Observer 監測 BeforeWaiting (RunLoop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取全部剛被標記爲待處理的 GestureRecognizer,並執行 GestureRecognizer 的回調。 當有 UIGestureRecognizer 的變化(建立/銷燬/狀態改變)時,這個回調都會進行相應處理。

  • 界面:

當在操做 UI 時,好比改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay 方法後,這個 UIView/CALayer 就被標記爲待處理,並被提交到一個全局的容器去。 系統註冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調去執行函數,會遍歷全部待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 界面。

  • CADisplayLink

能夠理解爲一個和屏幕刷新率一致的定時器。若是在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去,形成界面卡頓的感受。在快速滑動TableView時,即便一幀的卡頓也會讓用戶有所察覺。

4.4 RunLoop 在第三方庫的實際應用

  • AFNetworking

AFURLConnectionOperation 這個類是基於 NSURLConnection 構建的,其但願能在後臺線程接收 Delegate 回調。AFNetworking 單首創建了一個線程,並在這個線程中啓動了一個 RunLoop。

  • AsyncDisplayKit

Facebook 推出的用於保持界面流暢性的框架,將繪製和排版放在後臺線程進行。使用 Node 來封裝 View 和 Layer,並實現了相似的一套界面更新的機制:在主線程的 RunLoop 中添加一個 Observer,監聽了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回調時,遍歷全部以前放入隊列的待處理的任務,而後一一執行。

5. 關於RunLoop可供複習的精選文章

參考:

解密 Runloop
iOS學習——淺談RunLoop - 雲+社區 - 騰訊雲
深刻理解RunLoop | Garan no dou
iOS底層原理總結 - RunLoop - 掘金

源碼:CFRunLoop.c

相關文章
相關標籤/搜索