一遵倦、RunLoop概念
RunLoop是通過內(nèi)部維護(hù)的事件循環(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ù)一直沒有返回许赃,而是不斷地接收處理消息以及等待休眠株茶,所以運(yùn)行程序之后會保持持續(xù)運(yùn)行狀態(tài)。
二图焰、RunLoop的數(shù)據(jù)結(jié)構(gòu)
NSRunLoop(Foundation)
是CFRunLoop(CoreFoundation)
的封裝启盛,提供了面向?qū)ο蟮腁PI
RunLoop 相關(guān)的主要涉及五個類:
CFRunLoop
:RunLoop對象
CFRunLoopMode
:運(yùn)行模式
CFRunLoopSource
:輸入源/事件源
CFRunLoopTimer
:定時源
CFRunLoopObserver
:觀察者
1、CFRunLoop
由pthread
(線程對象技羔,說明RunLoop和線程是一一對應(yīng)的)僵闯、currentMode
(當(dāng)前所處的運(yùn)行模式)、modes
(多個運(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)前線程從內(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è)的時間點(diǎn)喚醒RunLoop執(zhí)行回調(diào)乒融。因?yàn)樗腔赗unLoop的掰盘,因此它不是實(shí)時的(就是NSTimer 是不準(zhǔn)確的。 因?yàn)镽unLoop只負(fù)責(zé)分發(fā)源的消息簇抵。如果線程當(dāng)前正在處理繁重的任務(wù)庆杜,就有可能導(dǎo)致Timer本次延時射众,或者少執(zhí)行一次)碟摆。
5、CFRunLoopObserver
監(jiān)聽以下時間點(diǎn):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的Mode
關(guān)于Mode首先要知道一個RunLoop 對象中可能包含多個Mode轩缤,且每次調(diào)用 RunLoop 的主函數(shù)時,只能指定其中一個 Mode(CurrentMode)贩绕。切換 Mode火的,需要重新指定一個 Mode 。主要是為了分隔開不同的 Source淑倾、Timer馏鹤、Observer,讓它們之間互不影響娇哆。
當(dāng)RunLoop運(yùn)行在Mode1上時湃累,是無法接受處理Mode2或Mode3上的Source、Timer碍讨、Observer事件的
總共是有五種CFRunLoopMode
:
kCFRunLoopDefaultMode
:默認(rèn)模式治力,主線程是在這個運(yùn)行模式下運(yùn)行UITrackingRunLoopMode
:跟蹤用戶交互事件(用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他Mode影響)UIInitializationRunLoopMode
:在剛啟動App時第進(jìn)入的第一個 Mode勃黍,啟動完成后就不再使用GSEventReceiveRunLoopMode
:接受系統(tǒng)內(nèi)部事件琴许,通常用不到kCFRunLoopCommonModes
:偽模式,不是一種真正的運(yùn)行模式溉躲,是同步Source/Timer/Observer到多個Mode中的一種解決方案
四榜田、RunLoop的實(shí)現(xiàn)機(jī)制
這張圖在網(wǎng)上流傳比較廣益兄。
對于RunLoop而言最核心的事情就是保證線程在沒有消息的時候休眠,在有消息時喚醒箭券,以提高程序性能净捅。RunLoop這個機(jī)制是依靠系統(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)核實(shí)現(xiàn)的mach_msg()
函數(shù)會完成實(shí)際的工作豆村。
即基于port的source1液兽,監(jiān)聽端口,端口有消息就會觸發(fā)回調(diào)掌动;而source0四啰,要手動標(biāo)記為待處理和手動喚醒RunLoop
Mach消息發(fā)送機(jī)制
大致邏輯為:
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與NSTimer
一個比較常見的問題:滑動tableView時卖毁,定時器還會生效嗎揖曾?
默認(rèn)情況下RunLoop運(yùn)行在kCFRunLoopDefaultMode
下,而當(dāng)滑動tableView時势篡,RunLoop切換到UITrackingRunLoopMode
翩肌,而Timer是在kCFRunLoopDefaultMode
下的模暗,就無法接受處理Timer的事件禁悠。
怎么去解決這個問題呢?把Timer添加到UITrackingRunLoopMode
上并不能解決問題兑宇,因?yàn)檫@樣在默認(rèn)情況下就無法接受定時器事件了碍侦。
所以我們需要把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)建的線程默認(rèn)是沒有開啟RunLoop的
1濒旦、怎么創(chuàng)建一個常駐線程?
1再登、為當(dāng)前線程開啟一個RunLoop(第一次調(diào)用 [NSRunLoop currentRunLoop]方法時實(shí)際是會先去創(chuàng)建一個RunLoop)
1尔邓、向當(dāng)前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史隆,然后添加到當(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");
});
3崩掘、怎樣保證子線程數(shù)據(jù)回來更新UI的時候不打斷用戶的滑動操作七嫌?
當(dāng)我們在子請求數(shù)據(jù)的同時滑動瀏覽當(dāng)前頁面,如果數(shù)據(jù)請求成功要切回主線程更新UI苞慢,那么就會影響當(dāng)前正在滑動的體驗(yàn)诵原。
我們就可以將更新UI事件放在主線程的NSDefaultRunLoopMode
上執(zhí)行即可,這樣就會等用戶不再滑動頁面挽放,主線程RunLoop由UITrackingRunLoopMode
切換到NSDefaultRunLoopMode
時再去更新UI
[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
收錄:原文地址
更多:iOS面試題合集