推薦閱讀:備戰(zhàn)2020——iOS全新面試題總結(jié)
RunLoop概念
RunLoop的數(shù)據(jù)結(jié)構(gòu)
RunLoop的Mode
RunLoop的實(shí)現(xiàn)機(jī)制
RunLoop與NSTimer
RunLoop和線程
一、RunLoop概念
RunLoop是通過(guò)內(nèi)部維護(hù)的事件循環(huán)(Event Loop)
來(lái)對(duì)事件/消息進(jìn)行管理
的一個(gè)對(duì)象。
1、沒(méi)有消息處理時(shí),休眠已避免資源占用,由用戶態(tài)切換到內(nèi)核態(tài)(CPU-內(nèi)核態(tài)和用戶態(tài))
2、有消息需要處理時(shí),立刻被喚醒,由內(nèi)核態(tài)切換到用戶態(tài)
為什么main函數(shù)不會(huì)退出?
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
UIApplicationMain內(nèi)部默認(rèn)開(kāi)啟了主線程的RunLoop,并執(zhí)行了一段無(wú)限循環(huán)的代碼(不是簡(jiǎn)單的for循環(huán)或while循環(huán))
//無(wú)限循環(huán)代碼模式(偽代碼)
int main(int argc, char * argv[]) {
BOOL running = YES;
do {
// 執(zhí)行各種任務(wù),處理各種事件
// ......
} while (running);
return 0;
}
UIApplicationMain函數(shù)一直沒(méi)有返回,而是不斷地接收處理消息以及等待休眠,所以運(yùn)行程序之后會(huì)保持持續(xù)運(yùn)行狀態(tài)。
二、RunLoop的數(shù)據(jù)結(jié)構(gòu)
NSRunLoop(Foundation)
是CFRunLoop(CoreFoundation)
的封裝,提供了面向?qū)ο蟮腁PI
RunLoop 相關(guān)的主要涉及五個(gè)類:
CFRunLoop
:RunLoop對(duì)象
CFRunLoopMode
:運(yùn)行模式
CFRunLoopSource
:輸入源/事件源
CFRunLoopTimer
:定時(shí)源
CFRunLoopObserver
:觀察者
1、CFRunLoop
由pthread
(線程對(duì)象,說(shuō)明RunLoop和線程是一一對(duì)應(yīng)的)、currentMode
(當(dāng)前所處的運(yùn)行模式)、modes
(多個(gè)運(yùn)行模式的集合)、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)喚醒線程,將當(dāng)前線程從內(nèi)核態(tài)切換到用戶態(tài) -
source1
:
基于port的,包含一個(gè) mach_port 和一個(gè)回調(diào),可監(jiān)聽(tīng)系統(tǒng)端口和通過(guò)內(nèi)核和其他線程發(fā)送的消息,能主動(dòng)喚醒RunLoop,接收分發(fā)系統(tǒng)事件。
具備喚醒線程的能力
4、CFRunLoopTimer
基于時(shí)間的觸發(fā)器,基本上說(shuō)的就是NSTimer。在預(yù)設(shè)的時(shí)間點(diǎn)喚醒RunLoop執(zhí)行回調(diào)。因?yàn)樗腔赗unLoop的,因此它不是實(shí)時(shí)的(就是NSTimer 是不準(zhǔn)確的。 因?yàn)镽unLoop只負(fù)責(zé)分發(fā)源的消息。如果線程當(dāng)前正在處理繁重的任務(wù),就有可能導(dǎo)致Timer本次延時(shí),或者少執(zhí)行一次)。
5、CFRunLoopObserver
監(jiān)聽(tīng)以下時(shí)間點(diǎn):CFRunLoopActivity
-
kCFRunLoopEntry
RunLoop準(zhǔn)備啟動(dòng) -
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īng)所有狀態(tài)
6、各數(shù)據(jù)結(jié)構(gòu)之間的聯(lián)系
線程和RunLoop一一對(duì)應(yīng), RunLoop和Mode是一對(duì)多的,Mode和source、timer、observer也是一對(duì)多的
三、RunLoop的Mode
關(guān)于Mode首先要知道一個(gè)RunLoop 對(duì)象中可能包含多個(gè)Mode,且每次調(diào)用 RunLoop 的主函數(shù)時(shí),只能指定其中一個(gè) Mode(CurrentMode)。切換 Mode,需要重新指定一個(gè) Mode 。主要是為了分隔開(kāi)不同的 Source、Timer、Observer,讓它們之間互不影響。
當(dāng)RunLoop運(yùn)行在Mode1上時(shí),是無(wú)法接受處理Mode2或Mode3上的Source、Timer、Observer事件的
總共是有五種CFRunLoopMode
:
kCFRunLoopDefaultMode
:默認(rèn)模式,主線程是在這個(gè)運(yùn)行模式下運(yùn)行UITrackingRunLoopMode
:跟蹤用戶交互事件(用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他Mode影響)UIInitializationRunLoopMode
:在剛啟動(dòng)App時(shí)第進(jìn)入的第一個(gè) Mode,啟動(dòng)完成后就不再使用GSEventReceiveRunLoopMode
:接受系統(tǒng)內(nèi)部事件,通常用不到kCFRunLoopCommonModes
:偽模式,不是一種真正的運(yùn)行模式,是同步Source/Timer/Observer到多個(gè)Mode中的一種解決方案
四、RunLoop的實(shí)現(xiàn)機(jī)制
這張圖在網(wǎng)上流傳比較廣。
對(duì)于RunLoop而言最核心的事情就是保證線程在沒(méi)有消息的時(shí)候休眠,在有消息時(shí)喚醒,以提高程序性能。RunLoop這個(gè)機(jī)制是依靠系統(tǒng)內(nèi)核來(lái)完成的(蘋果操作系統(tǒng)核心組件Darwin中的Mach)。
RunLoop通過(guò)mach_msg()
函數(shù)接收、發(fā)送消息。它的本質(zhì)是調(diào)用函數(shù)mach_msg_trap()
,相當(dāng)于是一個(gè)系統(tǒng)調(diào)用,會(huì)觸發(fā)內(nèi)核狀態(tài)切換。在用戶態(tài)調(diào)用 mach_msg_trap()
時(shí)會(huì)切換到內(nèi)核態(tài);內(nèi)核態(tài)中內(nèi)核實(shí)現(xiàn)的mach_msg()
函數(shù)會(huì)完成實(shí)際的工作。
即基于port的source1,監(jiān)聽(tīng)端口,端口有消息就會(huì)觸發(fā)回調(diào);而source0,要手動(dòng)標(biāo)記為待處理和手動(dòng)喚醒RunLoop
Mach消息發(fā)送機(jī)制
大致邏輯為:
1、通知觀察者 RunLoop 即將啟動(dòng)。
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ā)生才喚醒線程。
- 一個(gè)基于 port 的Source1 的事件(圖里應(yīng)該是source0)。
- 一個(gè) Timer 到時(shí)間了。
- RunLoop 自身的超時(shí)時(shí)間到了。
- 被其他調(diào)用者手動(dòng)喚醒。
8、通知觀察者線程將被喚醒。
9、處理喚醒時(shí)收到的事件。
- 如果用戶定義的定時(shí)器啟動(dòng),處理定時(shí)器事件并重啟RunLoop。進(jìn)入步驟2。
- 如果輸入源啟動(dòng),傳遞相應(yīng)的消息。
- 如果RunLoop被顯示喚醒而且時(shí)間還沒(méi)超時(shí),重啟RunLoop。進(jìn)入步驟2
10、通知觀察者RunLoop結(jié)束。
五、RunLoop與NSTimer
一個(gè)比較常見(jiàn)的問(wèn)題:滑動(dòng)tableView時(shí),定時(shí)器還會(huì)生效嗎?
默認(rèn)情況下RunLoop運(yùn)行在kCFRunLoopDefaultMode
下,而當(dāng)滑動(dòng)tableView時(shí),RunLoop切換到UITrackingRunLoopMode
,而Timer是在kCFRunLoopDefaultMode
下的,就無(wú)法接受處理Timer的事件。
怎么去解決這個(gè)問(wèn)題呢?把Timer添加到UITrackingRunLoopMode
上并不能解決問(wèn)題,因?yàn)檫@樣在默認(rèn)情況下就無(wú)法接受定時(shí)器事件了。
所以我們需要把Timer同時(shí)添加到UITrackingRunLoopMode
和kCFRunLoopDefaultMode
上。
那么如何把timer同時(shí)添加到多個(gè)mode上呢?就要用到NSRunLoopCommonModes
了
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Timer就被添加到多個(gè)mode上,這樣即使RunLoop由kCFRunLoopDefaultMode
切換到UITrackingRunLoopMode
下,也不會(huì)影響接收Timer事件
六、RunLoop和線程
- 線程和RunLoop是一一對(duì)應(yīng)的,其映射關(guān)系是保存在一個(gè)全局的 Dictionary 里
- 自己創(chuàng)建的線程默認(rèn)是沒(méi)有開(kāi)啟RunLoop的
1、怎么創(chuàng)建一個(gè)常駐線程?
1、為當(dāng)前線程開(kāi)啟一個(gè)RunLoop(第一次調(diào)用 [NSRunLoop currentRunLoop]方法時(shí)實(shí)際是會(huì)先去創(chuàng)建一個(gè)RunLoop)
1、向當(dāng)前RunLoop中添加一個(gè)Port/Source等維持RunLoop的事件循環(huán)(如果RunLoop的mode中一個(gè)item都沒(méi)有,RunLoop會(huì)退出)
2、啟動(dòng)該RunLoop
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
2、輸出下邊代碼的執(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方法并不會(huì)執(zhí)行。
原因是如果是帶afterDelay的延時(shí)函數(shù),會(huì)在內(nèi)部創(chuàng)建一個(gè) NSTimer,然后添加到當(dāng)前線程的RunLoop中。也就是如果當(dāng)前線程沒(méi)有開(kāi)啟RunLoop,該方法會(huì)失效。
那么我們改成:
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中一個(gè)item都沒(méi)有,RunLoop會(huì)退出。即在調(diào)用RunLoop的run方法后,由于其mode中沒(méi)有添加任何item去維持RunLoop的時(shí)間循環(huán),RunLoop隨即還是會(huì)退出。
所以我們自己?jiǎn)?dòng)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");
});
3、怎樣保證子線程數(shù)據(jù)回來(lái)更新UI的時(shí)候不打斷用戶的滑動(dòng)操作?
當(dāng)我們?cè)谧诱?qǐng)求數(shù)據(jù)的同時(shí)滑動(dòng)瀏覽當(dāng)前頁(yè)面,如果數(shù)據(jù)請(qǐng)求成功要切回主線程更新UI,那么就會(huì)影響當(dāng)前正在滑動(dòng)的體驗(yàn)。
我們就可以將更新UI事件放在主線程的NSDefaultRunLoopMode
上執(zhí)行即可,這樣就會(huì)等用戶不再滑動(dòng)頁(yè)面,主線程RunLoop由UITrackingRunLoopMode
切換到NSDefaultRunLoopMode
時(shí)再去更新UI
[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];