iOS Runloop(面試題)

LeeJTom發表於2018-03-20

由一道題所引發的思考(抄書)。

NSRunLoop的以下描述錯誤的是()
A. Runloop並不是由系統自動控制的
B. 有3類物件可以被run loop監控:sources,timers,observers
C. 執行緒是預設啟動run loop的
D. NSTimer可手動新增到新建的NSRunLoop中

Runloop基本概念

Runloop 是什麼?Runloop 還是比較顧名思義的一個東西,說白了就是一種迴圈,只不過它這種迴圈比較高階。一般的 while 迴圈會導致 CPU 進入忙等待狀態,而 Runloop 則是一種“閒”等待,這部分可以類比 Linux 下的 epoll。當沒有事件時,Runloop 會進入休眠狀態,有事件發生時, Runloop 會去找對應的 Handler 處理事件。Runloop 可以讓執行緒在需要做事的時候忙起來,不需要的話就讓執行緒休眠。

蘋果官方文件的圖:

iOS Runloop(面試題)

圖中展現了 Runloop 線上程中的作用:從 input source 和 timer source 接受事件,然後線上程中處理事件。

Runloop 與執行緒

  • Runloop 和執行緒是繫結在一起的。每個執行緒(包括主執行緒)都有一個對應的 Runloop 物件。我們並 不能自己建立 Runloop 物件,但是可以獲取到系統提供的 Runloop 物件。
  • 主執行緒的 Runloop 會在 應用啟動的時候完成啟動其他執行緒的 Runloop 預設並不會啟動,需要我們 手動啟動

Input Source 和 Timer Source

這兩個都是 Runloop 事件的來源,其中 Input Source 又可以分為三類:

  • Port-Based Sources,系統底層的 Port 事件,例如 CFSocketRef ,在應用層基本用不到
  • Custom Input Sources,使用者手動建立的 Source
  • Cocoa Perform Selector Sources, Cocoa 提供的 performSelector 系列方法,也是一種事件源

Timer Source 顧名思義就是指定時器事件了。

Runloop Observer(CFRunLoopActivity)

Runloop 通過監控 Source 來決定有沒有任務要做,除此之外,我們還可以用 Runloop Observer 來監控 Runloop 本身的狀態。 Runloop Observer 可以監控下面的 runloop 事件:

CFRunLoopObserver是觀察者,可以監聽runLoop的狀態改變

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { 
kCFRunLoopEntry = (1UL << 0), //即將進入Runloop
kCFRunLoopBeforeTimers = (1UL << 1), //即將處理NSTimer 
kCFRunLoopBeforeSources = (1UL << 2), //即將處理Sources 
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠 
kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中喚醒 
kCFRunLoopExit = (1UL << 7), //即將退出runloop 
kCFRunLoopAllActivities = 0x0FFFFFFFU //所有狀態改變
};
複製程式碼

Runloop Mode

在監視與被監視中,Runloop 要處理的事情還挺複雜的。為了讓 Runloop 能專心處理自己關心的那部分事情,引入了 Runloop Mode 概念。

iOS Runloop(面試題)
如圖所示,Runloop Mode 實際上是 Source,Timer 和 Observer 的集合,不同的 Mode 把不同組的 Source,Timer 和 Observer 隔絕開來。Runloop 在某個時刻只能跑在一個 Mode 下,處理這一個 Mode 當中的 Source,Timer 和 Observer。

蘋果文件中提到的 Mode 有五個,分別是:

  • NSDefaultRunLoopMode
  • NSConnectionReplyMode
  • NSModalPanelRunLoopMode
  • NSEventTrackingRunLoopMode
  • NSRunLoopCommonModes

iOS 中公開暴露出來的只有 NSDefaultRunLoopModeNSRunLoopCommonModes
NSRunLoopCommonModes 實際上是一個 Mode 的 集合,預設包括 NSDefaultRunLoopModeNSEventTrackingRunLoopMode

與 Runloop 相關的坑

常開發中,與 runLoop 接觸得最近可能就是通過 NSTimer 了。一個 Timer 一次只能加入到一個 RunLoop 中。我們日常使用的時候,通常就是加入到當前的 runLoop 的 default mode 中,而 ScrollView 在使用者滑動時,主執行緒 RunLoop 會轉到 UITrackingRunLoopMode 。而這個時候, Timer 就不會執行。

有如下兩種解決方案:

  • 第一種: 設定RunLoop Mode,例如NSTimer,我們指定它執行於 NSRunLoopCommonModes ,這是一個Mode的集合。註冊到這個 Mode 下後,無論當前 runLoop 執行哪個 mode ,事件都能得到執行。
  • 第二種: 另一種解決Timer的方法是,我們在另外一個執行緒執行和處理 Timer 事件,然後在主執行緒更新UI。

在 AFNetworking 3.1中,就有相關的程式碼,如下:

//AFNetworkActivityIndicatorManager.m l:227
- (void)startActivationDelayTimer {
    self.activationDelayTimer = [NSTimer
                                 timerWithTimeInterval:self.activationDelay target:self selector:@selector(activationDelayTimerFired) userInfo:nil repeats:NO];
    [[NSRunLoop mainRunLoop] addTimer:self.activationDelayTimer forMode:NSRunLoopCommonModes];
}

複製程式碼

這裡就是新增了一個計時器,由於指定了 NSRunLoopCommonModes,所以不管 RunLoop 出於什麼狀態,都執行這個計時器任務。

關於文章提到的題目

選項C是錯誤的

C. 執行緒是預設啟動run loop的

因為:主執行緒的 Runloop 會在 應用啟動的時候完成啟動其他執行緒的 Runloop 預設並不會啟動,需要我們 手動啟動

參考資料

Runloop

相關文章