由一道題所引發的思考(抄書)。
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 可以讓執行緒在需要做事的時候忙起來,不需要的話就讓執行緒休眠。
蘋果官方文件的圖:
圖中展現了 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 概念。
如圖所示,Runloop Mode 實際上是 Source,Timer 和 Observer 的集合,不同的 Mode 把不同組的 Source,Timer 和 Observer 隔絕開來。Runloop 在某個時刻只能跑在一個 Mode 下,處理這一個 Mode 當中的 Source,Timer 和 Observer。蘋果文件中提到的 Mode 有五個,分別是:
- NSDefaultRunLoopMode
- NSConnectionReplyMode
- NSModalPanelRunLoopMode
- NSEventTrackingRunLoopMode
- NSRunLoopCommonModes
iOS 中公開暴露出來的只有 NSDefaultRunLoopMode
和 NSRunLoopCommonModes
。
NSRunLoopCommonModes 實際上是一個 Mode 的 集合,預設包括 NSDefaultRunLoopMode
和 NSEventTrackingRunLoopMode
。
與 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 預設並不會啟動,需要我們 手動啟動。
參考資料