iOS Runloop

1、Runloop 簡介

1. 簡介

  • RunLoop就是讓線程隨時處理事件但不退出的機制
  • 每個線程都有一個RunLoop
  • RunLoop 實際上就是一個對象,這個對象管理了其須要處理的事件(好比button的點擊、各類手勢的的事件、定時器、tableView的代理方法)和消息,是iOS裏的一種事件處理機制。
  • 線程執行了這個函數後,就會一直處於這個函數內部 「接受消息->等待->處理」 的循環中,直到這個循環結束(好比傳入 quit 的消息),函數返回。

2. 基本做用

  • 保持程序的持續運行(好比主運行循環)
  • 處理App中的各類事件(好比觸摸事件、定時器事件、Selector事件)
  • 節省CPU資源,提升程序性能:該作事時作事,該休息時休息

3. API

OSX / iOS 系統中,有2套API來訪問和使用 RunLoophtml

  • CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,全部這些 API 都是線程安全的。
  • NSRunLoop 是基於 CFRunLoopRef 的封裝,提供了面向對象的 API,可是這些 API 不是線程安全的。因此要了解 RunLoop 內部結構,須要多研究 CFRunLoopRef 層面的API(Core Foundation 層面)

NSRunLoopCFRunLoopRef 都表明着 RunLoop 對象。git

4. 存在價值

main 函數中的 RunLoop (主運行循環):第14行代碼的 UIApplicationMain 函數內部就啓動了一個 RunLoop。因此 UIApplicationMain 函數一直沒有返回,保持了程序的持續運行。這個默認啓動的 RunLoop 是跟主線程相關聯的。 程序員

image.jpeg

2、Runloop 解析

1. Runloop 運行模式

一種 Runloop 運行模式就是一個要監控的 Input 和 Timer 事件源的集合或者是一個要通知的 Runloop 觀察者的集合。每次運 行Runloop,都要指定一個運行模式(顯示地或者隱式地)。在 Runloop 的運行期間,只有和當前運行模式相關的源才能被監控和容許發送事件。類似的,只有和當前運行模式相關的觀察者纔會被通知 Runloop 的行爲。和其餘模式相關的源會保留新的事件直到 Runloop 運行在了合適的模式纔會分發。github

在咱們的代碼中,咱們能夠經過字符串來標識模式。Cocoa和Core Foundation定義了一個默認模式和幾個普通的有用的模式,這些模式都是用字符串來標識的。咱們能夠用一個字符串當作名字來自定義一個模式,雖然咱們自定義模式的名字是隨意的,可是模式的內容不是隨意的,在咱們本身建立的要用的模式中至少要添加一個 Input 源、 Timer 源或者 Runloop 觀察者。面試

在 Runloop 的特殊階段咱們但是使用運行模式來過濾咱們不想要的源的事件,大多數的狀況下,Runloop 都運行在系統提供的默認模式下,然而 Model Panel 可能運行在「模式」模式,當運行在這個模式期間,只有和這個模式相關的事件源纔會發送事件到咱們的線程。對於第二線程來講,咱們一般使用自定義模式來阻止低優先級的事件源在其餘關鍵處理的時間內發送事件。安全

注意:運行模式不是根據事件類型劃分的,而是根據事件源劃分的。咱們不能經過模式來匹配鼠標按下事件或者鍵盤事件,可是咱們能夠用運行模式來監聽一組不一樣的Port、暫時掛起Timers或者改變當前被監控的事件源和Runloop觀察者。bash

下面列舉了一些Cocoa和Core Foundation定義的標準模式:網絡

  • NSDefaultRunLoopMode:默認的運行模式,用於大部分操做,除了NSConnection對象事件。
  • NSConnectionReplyMode:用來監控NSConnection對象的回覆的,不多可以用到。
  • NSModalPanelRunLoopMode:用於標明和Mode Panel相關的事件。
  • NSEventTrackingRunLoopMode:用於跟蹤觸摸事件觸發的模式(例如UIScrollView上下滾動)。
  • NSRunLoopCommonModes:是一個模式集合,當綁定一個事件源到這個模式集合的時候就至關於綁定到了集合內的每個模式。Cocoa 應用默認包含 Default、Panel、Event Tracking 模式,Core Foundation 只包含 Default 模式,咱們能夠經過 CFRunLoopAddCommonMode 添加模式。

2. Runloop 處理邏輯

Runloop接收來自兩種源的事件:app

  1. 輸入源(Input sources):傳遞異步消息,一般來自於其餘線程或者程序。
  2. 定時源(Timer sources):傳遞同步消息,在設定好的時間或者循環間斷地發生的事件。

這兩種事件源都是使用應用指定的事件處理方法來處理到達的事件。框架

下面的圖顯示了Runloop和事件源的概念結構。 Input sources異步的分發事件到響應的處理器,而後引發runUntilDate:(由線程相關的Runloop對象調用)方法退出。 Timer sources同步分發事件到相應的處理器可是不會引發Runloop退出。

RunLoop處理邏輯1-官方.png

RunLoop處理邏輯2-官方.png

RunLoop處理邏輯3-網友整理.png

備註:

  • 輸入源:每個須要Runloop處理事件的對象都有一個輸入源(InputSource),而且把這個輸入源添加到Runloop裏,每產生一個事件(好比用戶作了一個手勢、點了一個button、滑動了一下tableview、定時器到時)就把這個事件放到對應的輸入源。Runloop運行時循環檢查每個輸入源是否有事件須要處理,若是有事件要處理Runloop就就調用這個事件的處理方法(經過addTargetxxx指定的方法或者是代理的方法)。若是Runloop裏全部的輸入源都沒有事件要處理,Runloop會休眠。若是Runloop裏一個輸入源都沒有(對象銷燬前會把它以前添加的那個輸入源取消),Runloop(runUntilDate:這個方法)就退出來了。
  • 除了處理輸入源的事件,Runloop也會生成Runloop行爲的通知。註冊Runloop的觀察者能夠收到這些消息,而後在線程內用他們作一些額外的處理。咱們只能使用Core Foundation接口來註冊線程的Runloop觀察者

3. Input Sources

Input Sources 異步地分發事件到線程。大概有兩種類型的 Input Sources,Port-based類型的輸入源監控着應用的Mach端口,自定義的輸入源監控着自定義的事件源。NSRunloop不關心輸入源的類型。兩種輸入源惟一的不一樣是輸入源的觸發方式,Port-based輸入源是由系統內核觸發的,而自定義的輸入源要咱們本身觸發。建立輸入源的時候咱們就給給輸入源添加指定的模式。下面是一些輸入源:

  • Port-Based Sources
    Cocoa 和 Core Foundation 提供了類和接口用來建立 Port-Based 源,Cocoa 只要建立 NSPort 對象,並添加到 NSRunloop 中就能夠啦,NSPort負責輸入源的建立和配置。Core Foundation 須要手動的常見 port 和輸入源。

  • Custom Input Sources
    咱們要用到CFRunLoopSourceRef函數建立輸入源,並定義幾個回調函數用於配置輸入源、處理事件和刪除輸入源。事件的觸發機制要咱們本身定義。

  • Cocoa Perform Selector Sources
    Cocoa定義了能夠在任何線程上執行方法的事件源,在想要執行的線程上執行方法是順序執行的,避免了多個方法在線程上執行的同步問題。Perform Selector Sources在方法執行完以後就會本身從NSRunloop中刪除。
    Perform Selector Sources要求目標線程的NSRunloop必須是運行的,主線程默認是運行的。NSRunloop在一次迭代過程當中會處理全部的Perform Selector調用,而不是一次迭代處理一個Perform Selector調用。NSObject中定義的Perform Selector方法以下

    • performSelectorOnMainThread:withObject:waitUntilDone:
    • performSelectorOnMainThread:withObject:waitUntilDone:modes:
    • performSelector:onThread:withObject:waitUntilDone:
    • performSelector:onThread:withObject:waitUntilDone:modes:
    • performSelector:withObject:afterDelay:
    • performSelector:withObject:afterDelay:inModes:
    • cancelPreviousPerformRequestsWithTarget:
    • cancelPreviousPerformRequestsWithTarget:selector:object:

    延遲執行是在NSRunloop的下一次迭代中過了指定的延遲事件才執行。取消操做是針對延遲執行方法的。

4. Timer Sources

Timer Sources 同步地在未來的一個肯定的時間分發事件到咱們的線程。Timers 可讓線程通知本身去處理一些事情。Timers 不是一個實時的機制,當 Timers 觸發的時候 NSrunloop 恰好正在執行處理函數,Timer s會等待 NSRunloop 調用本身的處理函數。

Timers 能夠建立一次性的和重複性的事件,當建立重複性的事件的時候,Timers 只會根據規劃好的觸發時間來從新規劃觸發時間,而不是根據確切的觸發時間。並且因爲延遲觸發丟失了幾回觸發的話,Timers 只會補充一次觸發。

5. NSRunloop 觀察者

不像是事件源同樣在事件觸發的時候執行處理函數。NSRunloop 觀察者是在 NSRunloop 幾個執行的特定的點觸發。NSRunloop 能夠觀察的幾個事件是:

  • 進入 NSRunloop
  • NSRunloop 將要處理 Timer 事件
  • NSRunloop 將要處理 Input 事件
  • NSRunloop 將要進入睡眠
  • NSRunloop 被喚醒,可是是在處理事件以前
  • 退出 NSRunloop

建立觀察者的方法是 CFRunLoopObserverRef,咱們能夠通 過Core Foundation 方法添加到指定的 NSRunloop。觀察者也能夠建立一次性的和重複性的。一次性的觀察者觸發以後就會從 NSRunloo p中刪除。

3、RunLoop 相關類

Core Foundation 中關於 RunLoop 的5個類

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef 注:RunLoop 若是沒有這些東西會直接退出

1. CFRunLoopModeRef

CFRunLoopModeRef表明RunLoop的運行模式:一個 RunLoop 包含若干個 Mode,每一個Mode又包含若干個 Source/Timer/Observer
每次RunLoop啓動時,只能指定其中一個 Mode,這個Mode被稱做 CurrentMode 若是須要切換 Mode,只能退出 Loop,再從新指定一個 Mode 進入 這樣作主要是爲了分隔開不一樣組的 Source/Timer/Observer,讓其互不影響。

1465700097876160.jpg

系統默認註冊了5個Mode:(前兩個跟最後一個經常使用)

  • kCFRunLoopDefaultMode:App的默認Mode,一般主線程是在這個Mode下運行
  • UITrackingRunLoopMode:界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響
  • UIInitializationRunLoopMode: 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用
  • GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,一般用不到
  • kCFRunLoopCommonModes: 這是一個佔位用的Mode,不是一種真正的Mode

2. CFRunLoopSourceRef 事件源(輸入源)

按照官方文檔的分類:

  • Port-Based Sources (基於端口,跟其餘線程交互,經過內核發佈的消息)
  • Custom Input Sources (自定義)
  • Cocoa Perform Selector Sources (performSelector...方法)

按照函數調用棧的分類

  • Source0:非基於Port的,event事件,只含有回調,須要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記爲待處理,而後手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop。
  • Source1:基於Port的,包含了一個 mach_port 和一個回調,被用於經過內核和其餘線程相互發送消息,能主動喚醒 RunLoop 的線程。

函數調用棧

函數調用棧.png

3. CFRunLoopTimerRef

CFRunLoopTimerRef 是基於時間的觸發器,基本上說的就是 NSTimer (CADisplayLink 也是加到 RunLoop),它受 RunLoop 的 Mode 影響。
GCD的定時器不受 RunLoop 的 Mode 影響。

4. CFRunLoopObserverRef

CFRunLoopObserverRef是觀察者,可以監聽RunLoop的狀態改變 能夠監聽的時間點有如下幾個

可監聽狀態.png

使用

- (void)observer {
     // 建立observer
     CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
         NSLog(@"----監聽到RunLoop狀態發生改變---%zd", activity);
     });
     // 添加觀察者:監聽RunLoop的狀態
     CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
     // 釋放Observer
     CFRelease(observer);
 }
特別注意
 /*
     CF的內存管理(Core Foundation)
     1.凡是帶有Create、Copy、Retain等字眼的函數,建立出來的對象,都須要在最後作一次release
     * 好比CFRunLoopObserverCreate
     2.release函數:CFRelease(對象);
  */
複製代碼

4、runloop應用

  • NSTimer
  • PerformSelector
  • ImageView顯示
  • 須要讓線程執行週期性的工做(常駐線程)
  • 自動釋放池
  • 須要使用 Port 或者自定義 Input Source 與其餘線程進行通信
  • NSURLConnection 在子線程中發起異步請求

1. NSTimer (最多見RunLoop使用)

場景還原:拖拽時模式由 NSDefaultRunLoopMode 進入 UITrackingRunLoopMode ,NSTimer 再也不響應圖片中止輪播,將計時器改爲 NSRunLoopCommonModes 模式下兩種模式均可運行。

- (void)timer {
     NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
     // 定時器只運行在NSDefaultRunLoopMode下,一旦RunLoop進入其餘模式,這個定時器就不會工做
     // [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
     // 定時器只運行在UITrackingRunLoopMode下,一旦RunLoop進入其餘模式,這個定時器就不會工做
     // [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
     // 定時器會跑在標記爲common modes的模式下
     // 標記爲common modes的模式:UITrackingRunLoopMode和NSDefaultRunLoopMode兼容
     [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
 }
 - (void)timer2 {
     // 調用了scheduledTimer返回的定時器,已經自動被添加到當前runLoop中,並且是NSDefaultRunLoopMode
     NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
     // 修改模式
     [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
 }
複製代碼

2. ImageView

需求:當用戶在拖拽時(UI交互時)不顯示圖片,拖拽完成時顯示圖片

  • 方法1 監聽UIScrollerView滾動 (經過UIScrollViewDelegate監聽,此處再也不舉例)
  • 方法2 RunLoop 設置運行模式
    // 只在NSDefaultRunLoopMode模式下顯示圖片
    // inModes:設置運行模式
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
    複製代碼

3. 常駐線程 (重要)

應用場景: 常常在後臺進行耗時操做,如:監控聯網狀態,掃描沙盒等 不但願線程處理完事件就銷燬,保持常駐狀態

  • 第一種(推薦)
    開啓
    - (void)run {
       //addPort:添加端口(就是source)  forMode:設置模式
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
       //啓動RunLoop
         [[NSRunLoop currentRunLoop] run];
      /*
       //另外兩種啓動方式
         [NSDate distantFuture]:遙遠的將來  這種寫法跟上面的run是一個意思
         [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
         不設置模式
         [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
       */
     }
    複製代碼
    退出-退出當前線程
    [NSThread exit];
    複製代碼
  • 第二種(奇葩法)
    優勢:退出RunLoop比較方便-定義個標記 while(flag){...}
    - (void)run {
         while (1) {
             [[NSRunLoop currentRunLoop] run];
         }
     }
    複製代碼

4. 自動釋放池

在休眠前(kCFRunLoopBeforeWaiting)進行釋放,處理事件前建立釋放池,中間建立的對象會放入釋放池。
特別注意:在啓動 RunLoop 以前建議用 @autoreleasepool {...} 包裹。
意義:建立一個大釋放池,釋放 {} 期間建立的臨時對象,通常好的框架的做者都會這麼作。

屏幕快照 2018-10-04 14.40.44.png

- (void)execute {
     @autoreleasepool {
         NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
         [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
         [[NSRunLoop currentRunLoop] run];
}
複製代碼

5. 補充: GCD定時器

通常的NSTimer定時器由於受到RunLoop,會存在時間不許時的狀況。 上文有提到GCD不受RunLoop影響,下面簡單的說一下它的使用

/** 定時器(這裏不用帶*,由於 dispatch_source_t 就是個類,內部已經包含了*) */
 @property (nonatomic, strong) dispatch_source_t timer;
 int count = 0;
 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
     // 得到隊列
     // dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
     dispatch_queue_t queue = dispatch_get_main_queue();
     // 建立一個定時器(dispatch_source_t本質仍是個OC對象)
     self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
     // 設置定時器的各類屬性(幾時開始任務,每隔多長時間執行一次)
     // GCD的時間參數,通常是納秒 NSEC_PER_SEC(1秒 == 10的9次方納秒)
     // 什麼時候開始執行第一個任務
     // dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC) 比當前時間晚3秒
     dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
     uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
     dispatch_source_set_timer(self.timer, start, interval, 0);
     // 設置回調
     dispatch_source_set_event_handler(self.timer, ^{
         NSLog(@"------------%@", [NSThread currentThread]);
         count++;
 //        if (count == 4) {
 //            // 取消定時器
 //            dispatch_cancel(self.timer);
 //            self.timer = nil;
 //        }
     });
     // 啓動定時器
     dispatch_resume(self.timer);
 }
複製代碼

5、runloop 與線程

每條線程都有惟一的一個與之對應的 RunLoop 對象;
主線程的 RunLoop 已經自動建立好了,子線程的RunLoop須要主動建立;
RunLoop在第一次獲取時建立,在線程結束時銷燬;

  • 獲取RunLoop對象

    // 工做線程 須要程序員手工寫代碼讓runloop運行起來
    [NSRunLoop currentLoop]runUntilDate:]
    // Foundation
    [NSRunLoop currentRunLoop]; // 得到當前線程的RunLoop對象
    [NSRunLoop mainRunLoop]; // 得到主線程的RunLoop對象
    // Core Foundation
    CFRunLoopGetCurrent(); // 得到當前線程的RunLoop對象
    CFRunLoopGetMain(); // 得到主線程的RunLoop對象
    複製代碼
  • 線程安全性
    基於 Cocoa 的接口不是線程安全的,基於 Core Foundation 的接口是線程安全的。

6、RunLoop 面試題

  1. 什麼是RunLoop?

    • 其實它內部就是do-while循環,在這個循環內部不斷的處理各類任務(好比Source、Timer、Observer)。
    • 一個線程對應一個RunLoop,主線程的RunLoop默認已經啓動,子線程的RunLoop須要手動啓動(調用run方法) 。
    • RunLoop只能選擇一個Mode啓動,若是當前Mode中沒有任何Soure、Timer、Observer,那麼就直接退出RunLoop。
  2. 在開發中如何使用RunLoop?什麼應用場景?

    • 開啓一個常駐線程(讓一個子線程不進入消亡狀態,等待其餘線程發來消息,處理其餘事件)
    • 在子線程中開啓一個定時器
    • 在子線程中進行一些長期監控
    • 能夠控制定時器在特定模式下執行
    • 可讓某些事件(行爲、任務)在特定模式下執行
    • 能夠添加 Observer 監聽 RunLoop 的狀態,好比監聽點擊事件的處理(在全部點擊事件以前作一些事情)
  3. 在異步線程中下載不少圖片。若是失敗了,該如何處理?請結合runloop來談談解決方案?
    答:(提示:在異步線程中啓動一個runloop從新發送網絡圖片)
    (1)從新下載圖片
    (2)利用 runloop 的輸入源回到主線程刷新 UIImageView。

相關連接

蘋果官方文檔
CFRunLoop官方文檔
NSRunLoop官方文檔
CFRunLoopRef
NSRunloop的使用

相關文章
相關標籤/搜索