2019 iOS面試題大全---全方面剖析面試
RunLoop概念
RunLoop的數(shù)據(jù)結(jié)構(gòu)
RunLoop的Mode
RunLoop的實現(xiàn)機制
RunLoop與NSTimer
RunLoop和線程
一岔乔、RunLoop概念
RunLoop是通過內(nèi)部維護的事件循環(huán)(Event Loop)
來對事件/消息進行管理
的一個對象。
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)部默認開啟了主線程的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的數(shù)據(jù)結(jié)構(gòu)
NSRunLoop(Foundation)
是CFRunLoop(CoreFoundation)
的封裝要出,提供了面向?qū)ο蟮腁PI
RunLoop 相關(guān)的主要涉及五個類:
CFRunLoop
:RunLoop對象
CFRunLoopMode
:運行模式
CFRunLoopSource
:輸入源/事件源
CFRunLoopTimer
:定時源
CFRunLoopObserver
:觀察者
1鸳君、CFRunLoop
由pthread
(線程對象,說明RunLoop和線程是一一對應(yīng)的)患蹂、currentMode
(當前所處的運行模式)或颊、modes
(多個運行模式的集合)、commonModes
(模式名稱字符串集合)况脆、commonModelItems
(Observer,Timer,Source集合)構(gòu)成
2饭宾、CFRunLoopMode
由name批糟、source0格了、source1、observers徽鼎、timers構(gòu)成
3盛末、CFRunLoopSource
分為source0和source1兩種
-
source0
:
即非基于port的,也就是用戶觸發(fā)的事件否淤。需要手動喚醒線程悄但,將當前線程從內(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 是不準確的煞茫。 因為RunLoop只負責分發(fā)源的消息帕涌。如果線程當前正在處理繁重的任務(wù)摄凡,就有可能導致Timer本次延時,或者少執(zhí)行一次)蚓曼。
5亲澡、CFRunLoopObserver
監(jiān)聽以下時間點:CFRunLoopActivity
-
kCFRunLoopEntry
RunLoop準備啟動 -
kCFRunLoopBeforeTimers
RunLoop將要處理一些Timer相關(guān)事件 -
kCFRunLoopBeforeSources
RunLoop將要處理一些Source事件 -
kCFRunLoopBeforeWaiting
RunLoop將要進行休眠狀態(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的Mode
關(guān)于Mode首先要知道一個RunLoop 對象中可能包含多個Mode瑞凑,且每次調(diào)用 RunLoop 的主函數(shù)時末秃,只能指定其中一個 Mode(CurrentMode)。切換 Mode籽御,需要重新指定一個 Mode 练慕。主要是為了分隔開不同的 Source、Timer技掏、Observer铃将,讓它們之間互不影響。
當RunLoop運行在Mode1上時哑梳,是無法接受處理Mode2或Mode3上的Source劲阎、Timer、Observer事件的
總共是有五種CFRunLoopMode
:
kCFRunLoopDefaultMode
:默認模式鸠真,主線程是在這個運行模式下運行UITrackingRunLoopMode
:跟蹤用戶交互事件(用于 ScrollView 追蹤觸摸滑動悯仙,保證界面滑動時不受其他Mode影響)UIInitializationRunLoopMode
:在剛啟動App時第進入的第一個 Mode,啟動完成后就不再使用GSEventReceiveRunLoopMode
:接受系統(tǒng)內(nèi)部事件吠卷,通常用不到kCFRunLoopCommonModes
:偽模式锡垄,不是一種真正的運行模式,是同步Source/Timer/Observer到多個Mode中的一種解決方案
四祭隔、RunLoop的實現(xiàn)機制
這張圖在網(wǎng)上流傳比較廣货岭。
對于RunLoop而言最核心的事情就是保證線程在沒有消息的時候休眠,在有消息時喚醒疾渴,以提高程序性能千贯。RunLoop這個機制是依靠系統(tǒng)內(nèi)核來完成的(蘋果操作系統(tǒng)核心組件Darwin中的Mach)。
RunLoop通過
mach_msg()
函數(shù)接收程奠、發(fā)送消息丈牢。它的本質(zhì)是調(diào)用函數(shù)mach_msg_trap()
,相當于是一個系統(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师幕,要手動標記為待處理和手動喚醒RunLoop
Mach消息發(fā)送機制
大致邏輯為:
1粟按、通知觀察者 RunLoop 即將啟動。
2霹粥、通知觀察者即將要處理Timer事件灭将。
3、通知觀察者即將要處理source0事件后控。
4庙曙、處理source0事件。
5浩淘、如果基于端口的源(Source1)準備好并處于等待狀態(tài)捌朴,進入步驟9。
6张抄、通知觀察者線程即將進入休眠狀態(tài)砂蔽。
7、將線程置于休眠狀態(tài)署惯,由用戶態(tài)切換到內(nèi)核態(tài)左驾,直到下面的任一事件發(fā)生才喚醒線程。
- 一個基于 port 的Source1 的事件(圖里應(yīng)該是source0)泽台。
- 一個 Timer 到時間了什荣。
- RunLoop 自身的超時時間到了。
- 被其他調(diào)用者手動喚醒怀酷。
8、通知觀察者線程將被喚醒嗜闻。
9蜕依、處理喚醒時收到的事件。
- 如果用戶定義的定時器啟動琉雳,處理定時器事件并重啟RunLoop样眠。進入步驟2。
- 如果輸入源啟動翠肘,傳遞相應(yīng)的消息檐束。
- 如果RunLoop被顯示喚醒而且時間還沒超時,重啟RunLoop束倍。進入步驟2
10被丧、通知觀察者RunLoop結(jié)束盟戏。
五、RunLoop與NSTimer
一個比較常見的問題:滑動tableView時甥桂,定時器還會生效嗎柿究?
默認情況下RunLoop運行在kCFRunLoopDefaultMode
下,而當滑動tableView時黄选,RunLoop切換到UITrackingRunLoopMode
蝇摸,而Timer是在kCFRunLoopDefaultMode
下的,就無法接受處理Timer的事件办陷。
怎么去解決這個問題呢貌夕?把Timer添加到UITrackingRunLoopMode
上并不能解決問題,因為這樣在默認情況下就無法接受定時器事件了民镜。
所以我們需要把Timer同時添加到UITrackingRunLoopMode
和kCFRunLoopDefaultMode
上蜂嗽。
那么如何把timer同時添加到多個mode上呢?就要用到NSRunLoopCommonModes
了
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Timer就被添加到多個mode上殃恒,這樣即使RunLoop由kCFRunLoopDefaultMode
切換到UITrackingRunLoopMode
下植旧,也不會影響接收Timer事件
六、RunLoop和線程
- 線程和RunLoop是一一對應(yīng)的,其映射關(guān)系是保存在一個全局的 Dictionary 里
- 自己創(chuàng)建的線程默認是沒有開啟RunLoop的
1离唐、怎么創(chuàng)建一個常駐線程病附?
1、為當前線程開啟一個RunLoop(第一次調(diào)用 [NSRunLoop currentRunLoop]方法時實際是會先去創(chuàng)建一個RunLoop)
1亥鬓、向當前RunLoop中添加一個Port/Source等維持RunLoop的事件循環(huán)(如果RunLoop的mode中一個item都沒有完沪,RunLoop會退出)
2、啟動該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方法并不會執(zhí)行。
原因是如果是帶afterDelay的延時函數(shù)熟呛,會在內(nèi)部創(chuàng)建一個 NSTimer宽档,然后添加到當前線程的RunLoop中。也就是如果當前線程沒有開啟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");
});
3儡羔、怎樣保證子線程數(shù)據(jù)回來更新UI的時候不打斷用戶的滑動操作宣羊?
當我們在子請求數(shù)據(jù)的同時滑動瀏覽當前頁面璧诵,如果數(shù)據(jù)請求成功要切回主線程更新UI,那么就會影響當前正在滑動的體驗段只。
我們就可以將更新UI事件放在主線程的NSDefaultRunLoopMode
上執(zhí)行即可腮猖,這樣就會等用戶不再滑動頁面,主線程RunLoop由UITrackingRunLoopMode
切換到NSDefaultRunLoopMode
時再去更新UI
[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];