一、為什么 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:

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ā)送消息。
當(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 屬性的值。