RunLoop介紹
RunLoop是與線程相關(guān)的基本基礎(chǔ)結(jié)構(gòu)的一部分。RunLoop直譯為運行循環(huán),是線程內(nèi)用于運行事件處理以響應(yīng)傳入事件的一個循環(huán)。RunLoop的作用就是為了在有事件到達時喚醒線程以處理各種事件(如touch事件,計時器,performSelector等),事件處理完讓線程進入休眠以節(jié)省cpu資源。
RunLoop與線程是一一對應(yīng)的,主線程的Runloop默認是開啟,子線程需要手動開啟。為什么perfromSelector:withObject:afterDelay:在子線程為什么不好使?原因就是該方法會在當(dāng)前線程的RunLoop的defaultMode下添加一個Timer,而子線程RunLoop沒有開啟,因此RunLoop會直接退出不會執(zhí)行,可以在這個方法后面使用[[[NSRunLoop currentRunLoop] run];手動開啟解決。
RunLoop Modes
RunLoop有多種Mode,Mode是Input sources、Timer sources和Observers的集合。RunLoop在同一時刻只能在一種Mode下運行,并且只有在該Mode下的事件才能被處理,只有該Mode下的觀察者才會被發(fā)送通知。蘋果定義的常見Mode有五種:
- NSDefaultRunLoopMode:它是默認的,通常情況下都是在這個模式下運行
- NSConnectionReplyMode:用來監(jiān)控NSConnection對象的回復(fù)的,很少能夠用到
- NSModalPanelRunLoopMode:用于標(biāo)明和Mode Panel相關(guān)的事件
- NSEventTrackingRunLoopMode:用于跟蹤觸摸事件觸發(fā)的模式
- NSRunLoopCommonModes:這是一個可配置的模式,默認會包含default、modal、track三種,添加到這種模式下的事件在它包含的mode中都可以被處理,另外還可以使用
CFRunLoopAddCommonMode函數(shù)添加自定義模式。
RunLoop 輸入源
RunLoop從兩種不同的源接收事件,分別是Input sources和Timer sources。Input sources傳遞異步事件,通常是來自其他線程或其他應(yīng)用程序;Timer sources傳遞同步事件,以預(yù)定時間或重復(fù)間隔觸發(fā)。這兩種類型的源在事件到達時會使用特定的handler來處理事件。下圖是蘋果介紹Runloop與輸入源的經(jīng)典結(jié)構(gòu)圖

- Port-Based Sources:Cocoa中可以簡單的創(chuàng)建一個NSPort對象添加到RunLoop,port對象會處理所需輸入源的創(chuàng)建和配置;而在CoreFoundation中,必須手動創(chuàng)建端口及其運行循環(huán)源。
-
Custom Input Sources:自定義輸入源必須要使用CoreFoundation中的
CFRunLoopSourceRef相關(guān)聯(lián)的函數(shù) - Cocoa Perform Selector Sources:使用NSObject提供的performSelector方法
- Timer Sources:使用NSTimer或CFRunLoopTimer創(chuàng)建,Timer會有預(yù)定的時間觸發(fā),若為重復(fù)定時器,那么將會在指定的次數(shù)*時間間隔后執(zhí)行,哪怕是被延遲。如果觸發(fā)時間延遲太多以致錯過了一個或多個預(yù)定的觸發(fā)時間,則計時器在錯過的時間段內(nèi)只觸發(fā)一次,之后將被重新安排為下一個計劃的觸發(fā)時間。
RunLoop除了處理輸入源之外,還可以為RunLoop的行為生成了一些通知,為這些通知添加觀察者就可以在線程上做一些其他處理,注冊觀察者的api在Core Foundation框架中。如使用通過監(jiān)聽RunLoop的狀態(tài)變化來監(jiān)測主線程是否發(fā)生了卡頓。RunLoop Observer的幾種狀態(tài):
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),//啟動
kCFRunLoopBeforeTimers = (1UL << 1),//即將處理Timers
kCFRunLoopBeforeSources = (1UL << 2),//即將處理Sources
kCFRunLoopBeforeWaiting = (1UL << 5),//即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6),//休眠狀態(tài) 等待被喚醒
kCFRunLoopExit = (1UL << 7),//退出
kCFRunLoopAllActivities = 0x0FFFFFFFU //所有狀態(tài)集合
};

RunLoop原理
查看RunLoop的run底層源碼,當(dāng)CFRunLoopRunResult不等于stopped和finished時會執(zhí)行do...while循環(huán),調(diào)用CFRunLoopRunSpecific()函數(shù)
typedef CF_ENUM(SInt32, CFRunLoopRunResult) {
kCFRunLoopRunFinished = 1,
kCFRunLoopRunStopped = 2,
kCFRunLoopRunTimedOut = 3,
kCFRunLoopRunHandledSource = 4
};
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
可以看出主要邏輯就在CFRunLoopRunSpecific()函數(shù)中,其內(nèi)部主要邏輯為
- 首先根據(jù)modeName找到對應(yīng)的mode
- 通知Observers,RunLoop即將進入kCFRunLoopEntry
- 進入循環(huán),調(diào)用核心函數(shù)__CFRunLoopRun()
- 通知Observers,RunLoop即將退出kCFRunLoopExit
核心函數(shù)__CFRunLoopRun()內(nèi)部循環(huán)處理流程
- 0 啟動一個10^10的計時器,并開啟循環(huán)
- 1 通知Observers:將要處理Timers事件
- 2 通知Observers:將要處理Source0事件
- 3 執(zhí)行所有準(zhǔn)備就緒的Source0
- 4 如果有Source1準(zhǔn)備就緒,立即開始處理Source1
- 5 通知Observers:線程即將進入休眠
- 6 休眠,等待喚醒
- 7 休眠中被喚醒kCFRunLoopAfterWaiting,若為Timers則執(zhí)行1,若為source1則執(zhí)行4,若為GCD則執(zhí)行GCD回調(diào)函數(shù),若被其他調(diào)用者手動喚醒,則繼續(xù)從1開始循環(huán)
- 8 事件被處理或超時等改變了RunLoop狀態(tài)則跳出循環(huán),返回已處理/超時/停止/結(jié)束狀態(tài)。
通過源碼可以看到RunLoop處理事件的回調(diào)函數(shù)有六種,也可以通過debug斷點回調(diào),然后查看匯編代碼進行佐證,分別為:
- GCD主隊列:CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
- observer源:CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
- 調(diào)用timer:CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
- block應(yīng)用: CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
- source0:CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
- source1:CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
RunLoop相關(guān)面試題
- 如何創(chuàng)建一個常駐線程?
NSRunLoop無法通過alloc創(chuàng)建,直接在子線程中使用CFRunLoopGetCurrent()或[NSRunLoop currentRunLoop]在第一次時即會進行創(chuàng)建,隨后添加一個Port/Source到Runloop,最后調(diào)用run啟動。如果Runloop的mode中一個item都沒有,那么runloop會直接退出。
2.performSelector:withObject:afterDelay:這個方法在子線程中調(diào)用有什么問題?
會不起作用,因為這個方法的實現(xiàn)是在當(dāng)前的runloop的defaultmode下添加一個timer,因為runloop在子線程默認是不開啟的,因此不會執(zhí)行,可在這此方法之后添加調(diào)用[[NSRunLoop currentRunLoop] run];或者使用GCD來實現(xiàn).
- 利用runloop解釋界面的渲染過程?
在調(diào)用[UIView setNeedDisplay],或者直接調(diào)用調(diào)用[CAlayer setNeedsDisplay]時,相當(dāng)于給當(dāng)前的layer打上了一個臟標(biāo)記,標(biāo)識為需要重繪,但此時并沒有直接進行繪制。而是會在當(dāng)前的Runloop即將進行休眠時,即CFRunLoopBeforeWaiting狀態(tài)時才開始繪制。-
[CAlayer setNeedsDisplay]會調(diào)用[CALayer display] - 若layer沒有delegate,則會
[CALayer drawInContext:] - 有delegate判斷是否實現(xiàn)了
- (void)displayLayer:(CALayer *)layer異步繪制代理方法 - 若沒有實現(xiàn)異步繪制的代理方法,則會繼續(xù)進行系統(tǒng)繪制的流程,CALayer內(nèi)部會創(chuàng)建一個Backing Store用于獲取圖形上下文
- 調(diào)用delegate方法
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx,并返回- (void)drawRect:(CGRect)rect的回調(diào) - 最終 CALayer 都會將位圖提交到 Backing Store ,最后提交給 GPU,繪制結(jié)束
-
4.解釋一下事件響應(yīng)的過程?
蘋果注冊了一個基于mach port的source1用來接收系統(tǒng)事件,當(dāng)一個硬件事件發(fā)生(觸摸/搖晃/鎖屏等),首先會由系統(tǒng)的IOKit.framework封裝成一個IOHIDEvent,并且由內(nèi)核接受,內(nèi)核接收到事件之后會通過mach port轉(zhuǎn)發(fā)給App進程,隨后source1即會被觸發(fā),然后在回調(diào)的內(nèi)部會把IOHIDEvent處理并包裝成UIEvent事件,然后發(fā)送給UIWindow進行響應(yīng)處理。
5.autoreleasePool是在何時進行釋放?
App啟動后,蘋果在主線程的Runloop注冊了兩個observer,第一個observer監(jiān)視的RunLoop的Entry事件,在這個事件的回調(diào)內(nèi)會調(diào)用autoreleasePoolPush()創(chuàng)建自動釋放池,它的優(yōu)先級最高,保證創(chuàng)建的動作在其他所有回調(diào)之前;第二個observer監(jiān)視的RunLoop的BeforWaiting和Exit兩個事件,在BeforWaiting時調(diào)用autoreleasePoolPop()釋放舊池和autoreleasePoolPush()創(chuàng)建新池,在Exit時調(diào)用autoreleasePoolPop()釋放舊池,這個observer的優(yōu)先級最低,保證釋放發(fā)生在其他回調(diào)之后。