Runloop 面試題

一、為什么 NSTimer 有時候不好使?

因為創(chuàng)建的 NSTimer 默認(rèn)是被加入到了 defaultMode,所以當(dāng) Runloop 的 Mode 變化時,當(dāng)前的 NSTimer 就不會工作了。

二、AFNetworking 中如何運用 Runloop?

AFURLConnectionOperation 這個類是基于 NSURLConnection 構(gòu)建的,其希望能在后臺線程接收 Delegate 回調(diào)。為此 AFNetworking 單獨創(chuàng)建了一個線程,并在這個線程中啟動了一個 RunLoop

AFURLConnectionOperation

RunLoop 啟動前內(nèi)部必須要有至少一個 Timer/Observer/Source,所以 AFNetworking[runLoop run] 之前先創(chuàng)建了一個新的 NSMachPort 添加進(jìn)去了。通常情況下,調(diào)用者需要持有這個 NSMachPort(mach_port) 并在外部線程通過這個 port 發(fā)送消息到 loop 內(nèi);但此處添加 port 只是為了讓 RunLoop 不至于退出,并沒有用于實際的發(fā)送消息。
image.png

當(dāng)需要這個后臺線程執(zhí)行任務(wù)時,AFNetworking 通過調(diào)用 [NSObject performSelector:onThread:..] 將這個任務(wù)扔到了后臺線程的 RunLoop

三、autoreleasePool 在何時被釋放?

App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()。

  • 第一個 Observer 監(jiān)視的事件是 Entry(即將進(jìn)入Loop),其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池。其 order 是 -2147483647,優(yōu)先級最高,保 證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。
  • 第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準(zhǔn)備進(jìn)入休眠) 時調(diào)用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池; Exit(即將退出 Loop) 時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優(yōu)先級最低,保證其釋放池子發(fā)生在其他所有回調(diào)之后。
  • 在主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)、Timer回調(diào)內(nèi)的。這些回調(diào)會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著,所以不會出現(xiàn)內(nèi)存泄漏,開發(fā)者也不必顯示創(chuàng)建 Pool 了。
四、PerformSelector 的實現(xiàn)原理?

1、 當(dāng)調(diào)用 NSObject 的 performSelecter:afterDelay: 后,實際上其內(nèi)部會創(chuàng)建一個 Timer 并添加到當(dāng)前線程的 RunLoop 中。所以如果當(dāng)前線程沒有 RunLoop,則這個方法會失效。
2、 當(dāng)調(diào)用 performSelector:onThread: 時,實際上其會創(chuàng)建一個 Timer 加到對應(yīng)的線程去,同樣的,如果對應(yīng) 線程沒有 RunLoop 該方法也會失效。

五、PerformSelector:afterDelay:這個方法在子線程中是否起作用?為什么?怎么 解決?

不起作用,子線程默認(rèn)沒有 Runloop,也就沒有 Timer。
解決的辦法是可以使用 GCD 來實現(xiàn):Dispatch_after

六、RunLoop 的 Mode

關(guān)于 Mode 首先要知道一個 RunLoop 對象中可能包含多個 Mode,且每次調(diào)用 RunLoop 的主函數(shù)時,只能 指定其中一個 Mode(CurrentMode)。切換 Mode,需要重新指定一個 Mode 。主要是為了分隔開不同的 Source、Timer、Observer,讓它們之間互不影響。
當(dāng) RunLoop 運行在 Mode1 上時,是無法接受處理 Mode2 或 Mode3 上的 Source、Timer、Observer 事件的
總共是有五種CFRunLoopMode:

  • kCFRunLoopDefaultMode:默認(rèn)模式,主線程是在這個運行模式下運行
  • UITrackingRunLoopMode:跟蹤用戶交互事件(用于 ScrollView 追蹤觸摸滑動,保證界面滑 動時不受其他 Mode 影響)
  • UIInitializationRunLoopMode:在剛啟動 App 時第進(jìn)入的第一個 Mode,啟動完成后就不 再使用
  • GSEventReceiveRunLoopMode:接受系統(tǒng)內(nèi)部事件,通常用不到
  • kCFRunLoopCommonModes:偽模式,不是一種真正的運行模式,是同步Source/Timer/Observer 到多個 Mode 中的一種解決方案
七、RunLoop 的實現(xiàn)機制

對于 RunLoop 而言最核心的事情就是保證線程在沒有消息的時候休眠,在有消息時喚醒,以提高程序性能。 RunLoop 這個機制是依靠系統(tǒng)內(nèi)核來完成的(蘋果操作系統(tǒng)核心組件 Darwin 中的 Mach)。
RunLoop 通過mach_msg()函數(shù)接收、發(fā)送消息。它的本質(zhì)是調(diào)用函數(shù)mach_msg_trap(),相當(dāng)于是一 個系統(tǒng)調(diào)用,會觸發(fā)內(nèi)核狀態(tài)切換。在用戶態(tài)調(diào)用 mach_msg_trap()時會切換到內(nèi)核態(tài);內(nèi)核態(tài)中內(nèi)核 實現(xiàn)的mach_msg()函數(shù)會完成實際的工作。 即基于 port 的 source1,監(jiān)聽端口,端口有消息就會觸發(fā)回調(diào);而 source0,要手動標(biāo)記為待處理和手動喚 醒 RunLoop
大致邏輯為:
1、通知觀察者 RunLoop 即將啟動。
2、通知觀察者即將要處理 Timer 事件。
3、通知觀察者即將要處理 source0 事件。
4、處理 source0 事件。
5、如果基于端口的源(Source1)準(zhǔn)備好并處于等待狀態(tài),進(jìn)入步驟 9。
6、通知觀察者線程即將進(jìn)入休眠狀態(tài)。
7、將線程置于休眠狀態(tài),由用戶態(tài)切換到內(nèi)核態(tài),直到下面的任一事件發(fā)生才喚醒線程。

  • 一個基于 port 的 Source1 的事件(圖里應(yīng)該是 source0)。
  • 一個 Timer 到時間了。
  • RunLoop 自身的超時時間到了。
  • 被其他調(diào)用者手動喚醒。

8、通知觀察者線程將被喚醒。
9、處理喚醒時收到的事件。

  • 如果用戶定義的定時器啟動,處理定時器事件并重啟 RunLoop。進(jìn)入步驟 2。
  • 如果輸入源啟動,傳遞相應(yīng)的消息。
  • 如果 RunLoop 被顯示喚醒而且時間還沒超時,重啟 RunLoop。進(jìn)入步驟 2

10、通知觀察者 RunLoop 結(jié)束。

八、RunLoop 和線程
  • 線程和 RunLoop 是一一對應(yīng)的,其映射關(guān)系是保存在一個全局的 Dictionary 里
  • 自己創(chuàng)建的線程默認(rèn)是沒有開啟 RunLoop 的

怎么創(chuàng)建一個常駐線程?
1、為當(dāng)前線程開啟一個 RunLoop(第一次調(diào)用 [NSRunLoop currentRunLoop]方法時實際是會先去創(chuàng)建一
個 RunLoop)
2、向當(dāng)前 RunLoop 中添加一個 Port/Source 等維持 RunLoop 的事件循環(huán)(如果 RunLoop 的 mode 中一個 item 都沒有,RunLoop 會退出)
3、啟動該 RunLoop

@autoreleasepool {
  NSRunLoop *runLoop = [ NSRunLoop current RunLoop ];
  [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
  [runLoop run] ;
}

輸出下邊代碼的執(zhí)行順序

NSLog(@"1");
dispatch_async(dispatch_ get_global_queue(0, 0), ^{
  NSLog(@"2");
  [self performSelector :@selector(test) withobject :nil afterDelay:10];
  NSLog(@"3");
});
NSLog(@"4");
- (void)test
  NSLog(@"5");
}

答案是 1423,test 方法并不會執(zhí)行。
原因是如果是帶 afterDelay 的延時函數(shù),會在內(nèi)部創(chuàng)建一個 NSTimer,然后添加到當(dāng)前線程的 RunLoop 中。也就是如果當(dāng)前線程沒有開啟 RunLoop,該方法會失效。
那我們改成:

dispatch_async(dispatch_ get_global_queue(0, 0), ^{
  NSLog(@"2");
  [[NSRunLoop currentRunLoop] run];
  [self performSelector :@selector(test) withobject :nil afterDelay:10];
  NSLog(@"3");
});

然而 test 方法依然不執(zhí)行。
原因是如果 RunLoop 的 mode 中一個 item 都沒有,RunLoop 會退出。即在調(diào)用 RunLoop 的 run 方法后,由于其 mode 中沒有添加任何 item 去維持 RunLoop 的時間循環(huán),RunLoop 隨即還是會退出。所以我們自己啟動 RunLoop,一定要在添加 item 后

dispatch_async(dispatch_ get_global_queue(0, 0), ^{
  NSLog(@"2");
  [self performSelector :@selector(test) withobject :nil afterDelay:10];
  [[NSRunLoop currentRunLoop] run];
  NSLog(@"3");
});

怎樣保證子線程數(shù)據(jù)回來更新 UI 的時候不打斷用戶的滑動操作?
當(dāng)我們在子請求數(shù)據(jù)的同時滑動瀏覽當(dāng)前頁面,如果數(shù)據(jù)請求成功要切回主線程更新 UI,那么就會影響當(dāng)前正在滑動的體驗。
我們就可以將更新 UI 事件放在主線程的 NSDefaultRunLoopMode 上執(zhí)行即可,這樣就會等用戶不再滑動頁面,主線程 RunLoop 由 UITrackingRunLoopMode 切換到NSDefaultRunLoopMode 時再去更新 UI

[self performSelectorOnMainThread: @selector(reloadData) withobject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
九、RunLoop 的數(shù)據(jù)結(jié)構(gòu)

NSRunLoop(Foundation)是CFRunLoop(CoreFoundation)的封裝,提供了面向?qū)ο蟮?API RunLoop 相關(guān)的主要涉及五個類:

RunLoop相關(guān) 解釋
CFRunLoop RunLoop 對象
CFRunLoopMode 運行模式
CFRunLoopSource 輸入源/事件源
CFRunLoopTimer 定時源
CFRunLoopObserver 觀察者

1、CFRunLoop
pthread(線程對象,說明 RunLoop 和線程是一一對應(yīng)的)、currentMode(當(dāng)前所處的運行模式)、 modes(多個運行模式的集合)、commonModes(模式名稱字符串集合)、 commonModelItems(Observer,Timer,Source 集合)構(gòu)成
2、CFRunLoopMode
由 name、source0、source1、observers、timers 構(gòu)成
3、CFRunLoopSource
分為 source0 和 source1 兩種

  • source0: 即非基于 port 的,也就是用戶觸發(fā)的事件。需要手動喚醒線程,將當(dāng)前線程從內(nèi)核態(tài)切換到用戶態(tài)
  • source1: 基于 port 的,包含一個 mach_port 和一個回調(diào),可監(jiān)聽系統(tǒng)端口和通過內(nèi)核和其他線程發(fā)送的 消息,能主動喚醒 RunLoop,接收分發(fā)系統(tǒng)事件。 具備喚醒線程的能力

4、CFRunLoopTimer
基于時間的觸發(fā)器,基本上說的就是 NSTimer。在預(yù)設(shè)的時間點喚醒 RunLoop 執(zhí)行回調(diào)。因為它是基于 RunLoop 的,因此它不是實時的(就是 NSTimer 是不準(zhǔn)確的。 因為 RunLoop 只負(fù)責(zé)分發(fā)源的消息。如果 線程當(dāng)前正在處理繁重的任務(wù),就有可能導(dǎo)致 Timer 本次延時,或者少執(zhí)行一次)。
5、CFRunLoopObserver
監(jiān)聽以下時間點:CFRunLoopActivity

CFRunLoopActivity 時間點
kCFRunLoopEntry RunLoop 準(zhǔn)備啟動
kCFRunLoopBeforeTimers RunLoop 將要處理一些 Timer 相關(guān)事件
kCFRunLoopBeforeSources RunLoop 將要處理一些 Source 事件
kCFRunLoopBeforeWaiting RunLoop 將要進(jìn)行休眠狀態(tài),即將由用戶態(tài)切換到內(nèi)核態(tài)
kCFRunLoopAfterWaiting RunLoop 被喚醒,即從內(nèi)核態(tài)切換到用戶態(tài)后
kCFRunLoopExit RunLoop 退出
kCFRunLoopAllActivities 監(jiān)聽所有狀態(tài)

6、各數(shù)據(jù)結(jié)構(gòu)之間的聯(lián)系
線程和 RunLoop 一一對應(yīng), RunLoop 和 Mode 是一對多的,Mode 和 source、timer、observer 也是一對多 的

十、RunLoop 概念

RunLoop 是通過內(nèi)部維護的事件循環(huán)(Event Loop)來對事件/消息進(jìn)行管理的一個對象。
1、沒有消息處理時,休眠已避免資源占用,由用戶態(tài)切換到內(nèi)核態(tài)(CPU-內(nèi)核態(tài)和用戶態(tài))
2、有消息需要處理時,立刻被喚醒,由內(nèi)核態(tài)切換到用戶態(tài)

為什么 main 函數(shù)不會退出?

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

UIApplicationMain 內(nèi)部默認(rèn)開啟了主線程的 RunLoop,并執(zhí)行了一段無限循環(huán)的代碼(不是簡單的 for 循環(huán)或 while 循環(huán))

//無限瘤環(huán)代碼模式(為代碼)
int main(int argc, char * argv[]) {
  BOOL running = YES;
  do {
  //執(zhí)行各種任務(wù),處理各種事件
  // ...
  } while (running);
  return 0;
}

UIApplicationMain 函數(shù)一直沒有返回,而是不斷地接收處理消息以及等待休眠,所以運行程序之后會保持持續(xù)運行狀態(tài)。

十一、RunLoop 與 NSTimer

一個比較常見的問題:滑動 tableView 時,定時器還會生效嗎?
默認(rèn)情況下 RunLoop 運行在 kCFRunLoopDefaultMode 下,而當(dāng)滑動 tableView 時,RunLoop 切換到 UITrackingRunLoopMode,而 Timer 是在 kCFRunLoopDefaultMode 下的,就無法接受處理 Timer 的事件。怎么去解決這個問題呢?把 Timer 添加到 UITrackingRunLoopMode 上并不能解決問題,因為這樣在默認(rèn)情況下就無法接受定時器事件了。
所以我們需要把 Timer 同時添加到 UITrackingRunLoopMode 和 kCFRunLoopDefaultMode 上。那么如何把 timer 同時添加到多個 mode 上呢?就要用到 NSRunLoopCommonModes 了

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

Timer 就被添加到多個 mode 上,這樣即使 RunLoop 由 kCFRunLoopDefaultMode 切換到
UITrackingRunLoopMode 下,也不會影響接收 Timer 事件

十二、講一下 Observer ?
typedef CFOPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCF RunLoopEntry = (1UL 《< 0), //即將進(jìn)入Loop
    kCFRunLoopBeforeTimers = (1UL << 1) , //即將處理Timer
    kCF RunLoopBeforeSources = (1UL<< 2), //即將處理Source
    kCFRunLoopBeforewaiting = (1UL << 5), //即將進(jìn)入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中晚醒
    kCFRunLoopExit = (1UL << 7); //即將退出Loop
};
十三、解釋一下 NSTimer。
  • NSTimer 其實就是 CFRunLoopTimerRef,他們之間是 toll-freebridged 的。一個 NSTimer 注冊到 RunLoop 后,RunLoop 會為其重復(fù)的時間點注冊好事件。例如 10:00,10:10,10:20 這幾個時間點。RunLoop 為了節(jié)省資源,并不會在非常準(zhǔn)確的時間點回調(diào)這個Timer。Timer 有個屬性叫做 Tolerance(寬容度),標(biāo)示了當(dāng)時間點到后,容許有多少最大誤差。
  • 如果某個時間點被錯過了,例如執(zhí)行了一個很長的任務(wù),則那個時間點的回調(diào)也會跳過去,不會延后執(zhí)行。 就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。
  • CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現(xiàn)原理更復(fù)雜,和 NSTimer 并不一樣, 其內(nèi)部實際是操作了一個 Source)。如果在兩次屏幕刷新之間執(zhí)行了一個長任務(wù),那其中就會有一幀被跳過去(和 NSTimer 相似),造成界面卡頓的感覺。在快速滑動 TableView 時,即使一幀的卡頓也會讓用戶有所察覺。Facebook 開源的 AsyncDisplayLink 就是為了解決界面卡頓的問題,其內(nèi)部也用到了 RunLoop
十四、解釋一下 事件響應(yīng) 的過程?

蘋果注冊了一個 Source1(基于 mach port 的) 用來接收系統(tǒng)事件,其回調(diào)函數(shù)為__IOHIDEventSystemClientQueueCallback()。
當(dāng)一個硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由IOKit.framework生成一個 IOHIDEvent 事件并 由 SpringBoard 接收。這個過程的詳細(xì)情況可以參考這里。SpringBoard只接收按鍵(鎖屏/靜音等), 觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的 App 進(jìn)程。隨后蘋果注冊 的那個 Source1 就會觸發(fā)回調(diào),并調(diào)用 _UIApplicationHandleEventQueue() 進(jìn)行應(yīng)用內(nèi)部的分發(fā)。
_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進(jìn)行處理或分發(fā),其中包括識別 UIGesture/處理屏幕旋轉(zhuǎn)發(fā)送給 UIWindow 等。通常事件比如 UIButton 點擊、 touchesBegin/Move/End/Cancel 事件都是在這個回調(diào)中完成的。

十五、解釋一下 手勢識別 的過程?

當(dāng)上面的 _UIApplicationHandleEventQueue()識別了一個手勢時,其首先會調(diào)用 Cancel 將當(dāng)前 的 touchesBegin/Move/End 系列回調(diào)打斷。隨后系統(tǒng)將對應(yīng)的 UIGestureRecognizer標(biāo)記為待 處理。
蘋果注冊了一個 Observer 監(jiān)測 BeforeWaiting(Loop 即將進(jìn)入休眠) 事件,這個 Observer 的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver(),其內(nèi)部會獲取所有剛被標(biāo)記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer 的回調(diào)。
當(dāng)有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時,這個回調(diào)都會進(jìn)行相應(yīng)處理。

十六、利用 runloop 解釋一下頁面的渲染的過程?

當(dāng)我們調(diào)用 [UIView setNeedsDisplay] 時,這時會調(diào)用當(dāng)前 View.layer[view.layer setNeedsDisplay]方法。
這等于給當(dāng)前的 layer 打上了一個臟標(biāo)記,而此時并沒有直接進(jìn)行繪制工作。而是會到當(dāng)前的 Runloop 即將休眠,也就是 beforeWaiting 時才會進(jìn)行繪制工作。
緊接著會調(diào)用 [CALayerdisplay],進(jìn)入到真正繪制的工作。 CALayer 層會判斷自己的 delegate 有 沒有實現(xiàn)異步繪制的代理方法 displayer:,這個代理方法是異步繪制的入口,如果沒有實現(xiàn)這個方法, 那么會繼續(xù)進(jìn)行系統(tǒng)繪制的流程,然后繪制結(jié)束。
CALayer 內(nèi)部會創(chuàng)建一個Backing Store,用來獲取圖形上下文。接下來會判斷這個 layer 是否有 delegate。
如果有的話,會調(diào)用 [layer.delegate drawLayer:inContext:],并且會返回給我們 [UIView DrawRect:]的回調(diào),讓我們在系統(tǒng)繪制的基礎(chǔ)之上再做一些事情。
如果沒有 delegate,那么會調(diào)用 [CALayer drawInContext:]
以上兩個分支,最終 CALayer 都會將位圖提交到 Backing Store,最后提交給 GPU。
至此繪制的過程結(jié)束。

十七、什么是異步繪制?

異步繪制,就是可以在子線程把需要繪制的圖形,提前在子線程處理好。將準(zhǔn)備好的圖像數(shù)據(jù)直接返給主線程使用,這樣可以降低主線程的壓力。

異步繪制的過程
要通過系統(tǒng)的 [view.delegate displayLayer:] 這個入口來實現(xiàn)異步繪制。

  • 代理負(fù)責(zé)生成對應(yīng)的 Bitmap
  • 設(shè)置該 Bitmap 為 layer.contents 屬性的值。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容