RunLoop運行循環基礎和應用

我們每天都在使用手機裡的各種APP,作為軟件開發者的我們,有沒有思考過這樣一個問題,某個APP開始運行後,如果對它沒有操作,它就會像靜止了,不會主動退出,也不會主動發生任何動作。但當我們觸動一下APP界面上的某個輸入框、按鈕等,這時就會有相應的響應事件發生。這個APP就像是一直待命,沒有操作時它在休息,有操作時立刻能做出響應。這就要歸功於RunLoop。


RunLoop的概念


RunLoop字面意思即運行循環,它是iOS的一個底層機制。在程序運行過程中循環做一些事,如果有RunLoop程序會一直運行,時刻等待著用戶的操作。

程序啟動時,以main函數為入口,main函數中進入一個自動釋放池,池中return了一個叫做UIApplicationMain的函數。


RunLoop運行循環基礎和應用


UIApplicationMain函數做了什麼事情呢?UIApplicationMain函數有4個參數,argc、argv前兩個參數是操作系統調用main函數,傳遞給UIApplicationMain 的必要參數,argc指參數的長度,argv指參數的value,第三個參數是應用程序對象所屬的類,該類必須繼承自UIApplication類,如果所屬類字符串的值為nil, UIKit就缺省使用UIApplication類,最後一個參數確定UIApplication的代理。以下是它的官方定義:


Declaration

int UIApplicationMain(int argc, char * _Nullable *argv, NSString *principalClassName, NSString *delegateClassName);

Summary

Creates the application object and the application delegate and sets up the event cycle.

Returns

Even though an integer return type is specified, this function never returns. When users exits an iOS app by pressing the Home button, the application moves to the background.

UIApplicationMain函數會建立起UIApplication類和代理,用來接收類似 didFinishLaunching 等與應用的生命週期相關的代理方法,並且建立起運行循環。雖然這個方法標示返回一個 int值,但它不會返回,它會一直存在於內存中,除非用戶或者系統將其強制終止。

回到文章最初的問題,應用程序為什麼不會退出?

因為UIApplicationMain函數會為main thread設置一個RunLoop對象,函數內部有一個保證應用程序不退出的死循環。當用戶點擊APP icon時,操作系統通過調用該應用程序的main函數來啟動該應用程序。首先操作系統會為應用程序開啟一條線程,CPU調度這條線程,這條線程是當前APP的主線程即常駐線程,之所以這條線程不會被釋放,因為這條線程上的RunLoop被開啟了,來保證應用程序不退出。


RunLoop的作用


通過如上解釋,我們知道了RunLoop能保證線程不退出;

其次,RunLoop負責監聽所有事件。比如時鐘事件、selector事件、觸摸事件等。

Runloop官方圖解:


RunLoop運行循環基礎和應用


RunLoop在循環過程中收到Input sources、Timer sources事件後,會交給對應的方法去處理,沒有事件傳入時,RunLoop會一直循環,等待用戶操作。

我們以時鐘事件為例:

我們創建NSTimer有以下方式:第一種通過timerWithTimeInterval方法,創建一個timer。

Declaration

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

Discussion

You must add the new timer to a run loop, using addTimer:forMode:. Then, after tiseconds have elapsed, the timer fires, sending the message aSelector to target. (If the timer is configured to repeat, there is no need to subsequently re-add the timer to the run loop.)

為了時鐘事件能夠響應,就必須把timer添加到當前的RunLoop中,從它的官方定義中就能看出。


RunLoop運行循環基礎和應用


第二種通過scheduledTimerWithTimeInterval方法, 創建一個timer,同樣能夠響應事件。

Declaration

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

Summary

Creates a timer and schedules it on the current run loop in the default mode.

官方定義中該方法內部已經封裝了將timer添加到當前的RunLoop。


RunLoop運行循環基礎和應用


RunLoop一直保持等待接收事件的狀態,當NSTimer添加到RunLoop運行循環中,它就會去處理時鐘事件,即每2秒觸發timerFire方法。

RunLoop在響應事件時,是分模式的。它可以在多種模式下進行切換,系統提供了5種模式。

NSDefaultRunLoopMode:默認模式

UITrackingRunLoopMode:UI界面模式

NSRunLoopCommonModes:佔位模式

UIInitializationRunLoopMode:程序初始化模式

GSEventReceiveRunLoopMode:系統內核模式


當我們創建timer時鐘事件把它加入RunLoop,設定運行模式為NSDefaultRunLoopMode時,在視圖滾動時,時鐘事件不會被觸發。因為RunLoop在處理UI觸摸事件,而忽略了時鐘事件。RunLoop會優先處理UITrackingRunLoopMode模式下的事件,。即UITrackingRunLoopMode優先級高於NSDefaultRunLoopMode。為了解決這個問題,我們可以設定RunLoop運行模式為NSRunLoopCommonModes, 它是一種佔位模式,代表在默認模式和UI模式下都添加某事件。


RunLoop對象


RunLoop不能直接被創建,但蘋果提供了自動獲取的方法

1. Core Foundation框架下的CFRunLoopRef對象,通過以下方式獲取當前線程與主線程的RunLoop對象:

CFRunLoopGetCurrent();

CFRunLoopGetMain();

2. Foundation框架下的NSRunLoop對象,是對CFRunLoopRef的一層OC的封裝。通過如下方式獲取:

[NSRunLoop currentRunLoop];

[NSRunLoop mainRunLoop];


RunLoop相關類


Runloop在Core Foundation框架下5 個類:

CFRunLoopRef

CFRunLoopModeRef

CFRunLoopSourceRef

CFRunLoopObserverRef

CFRunLoopTimerRef

圖示了這5個類的關係: 一個RunLoop對象包含若干個運行模式,每個運行模式由三種元素組成:Source(事件源)、Timer(定時器)、Observer(觀察者)。


RunLoop運行循環基礎和應用


CFRunLoopSourceRef

Source:即一切事件的來源,所有的事件都被包裝成source。叫做事件源(輸入源)

按照上文中列舉的Runloop官方圖解來分類:

1.port-Based Source 基於port端口系統內核事件

2.custom input Source 自定義事件源

3.cocoa perform selector sources

按照函數調用棧的分類

Source0:基於port的非系統內核事件,如觸摸事件、點擊事件

Source1:系統內核事件


CFRunLoopTimerRef

即定時器NSTimer

NSTimer可以通過timerWithTimeInterval:target:selector:userInfo:repeats創建。為了時鐘事件能夠響應,就必須手動把timer添加到當前的RunLoop中。

也可通過scheduledTimerWithTimeInterval:target:selector:userInfo:repeats創建,同樣能夠響應事件。因為方法內部已經封裝了將timer添加到RunLoop。


CFRunLoopObserverRef

觀察者,可以監聽 RunLoop 的狀態改變,包括kCFRunLoopBeforeSource, kCFRunLoopAfterWaiting等。我們可以通過製造NSTimer事件喚醒RunLoop去監聽它的狀態,在休眠時喚醒讓他去處理一下耗時任務等。


RunLoop與線程


線程是用來執行特定任務的,執行完成就會退出。但我們在實際開發中,可能會遇到這樣的情況,我們需要讓某個線程在某個特定條件下不退出,持續地處理任務。

舉例來說,NSTimer的回調事件裡有耗時操作,耗時操作是需要放入子線程的,且我們期望時鐘事件能被持續執行。


RunLoop運行循環基礎和應用


但是按照上面的寫法,timerFire方法中打印結果是沒有的,它並沒有被觸發。因為firstThread子線程在viewDidLoad方法執行結束後,已經被釋放了。我們知道,線程是由CPU調用和執行的,那線程怎樣保活,就是讓當前線程所在的RunLoop運行起來,線程才不會被釋放,一直被CPU所調度,時鐘事件才會被執行。這就需要我們在添加NSTimer到當前的RunLoop後,啟動它。


RunLoop運行循環基礎和應用


這樣,firstThread子線程就能一直不被釋放,去執行NSTimer的事件。

那當某個特定條件已經達成時,我們需要退出這個子線程,怎麼做呢?


RunLoop運行循環基礎和應用


我們可以在NSTimer回調方法裡,通過[NSThread exit]來直接退出線程。

所以,RunLoop與線程是息息相關的:每條線程都有唯一一個與之對應的Runloop對象;子線程的RunLoop在第一次獲取時創建,需要手動創建,在線程結束時銷燬。而主線程的RunLoop已經被操作系統創建並開啟了。


RunLoop的應用


上文我們講到RunLoop可以保活線程,多線程在我們開發時經常用到,一個子線程中的任務完成後會被銷燬,如果我們希望每次它不退出持續地執行任務時,就可以把它加入運行循環,從而避免了子線程頻繁的創建和銷燬。

通過把創建NSTimer對象添加到RunLoop,把運行模式設定為NSRunLoopCommonModes,來保證NSTimer在視圖滑動的情況下也正常計時。

另外,RunLoop還可以用來解決界面上處理耗時操作時的卡頓。

可以提前把耗時操作存入數組,然後為主線程的RunLoop添加觀察者CFRunLoopObserverRef,在觀察者的回調方法中執行數組中的單個耗時操作,即每次RunLoop循環只處理一次耗時操作,這樣單次循環的耗時變短,界面變得流暢。以下可做參考:


RunLoop運行循環基礎和應用

RunLoop運行循環基礎和應用


本文從App能隨時響應用戶交互引出RunLoop,介紹了它的概念、作用及應用等,如果你決定使用RunLoop,它的啟動很簡單,如果想研究更多,可以對應源碼去加深瞭解。


分享到:


相關文章: