iOS 里很重要的一個(gè)概念就是runloop泛领,到底什么是runloop呢?先從概念說起敛惊,如果大家接觸過node渊鞋,就會(huì)感到很熟悉,事件驅(qū)動(dòng),或者叫事件循環(huán)锡宋。
一儡湾、RunLoop概念
RunLoop是通過內(nèi)部維護(hù)的事件循環(huán)(Event Loop)來對(duì)事件/消息進(jìn)行管理的一個(gè)對(duì)象。
1执俩、沒有消息處理時(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)開啟了主線程的RunLoop摊崭,并執(zhí)行了一段無限循環(huán)的代碼(不是簡(jiǎn)單的for循環(huán)或while循環(huán))
UIApplicationMain函數(shù)一直沒有返回,而是不斷地接收處理消息以及等待休眠杰赛,所以運(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ì)象,說明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)端口和通過內(nèi)核和其他線程發(fā)送的消息混埠,能主動(dòng)喚醒RunLoop怠缸,接收分發(fā)系統(tǒng)事件。
具備喚醒線程的能力
4钳宪、CFRunLoopTimer
基于時(shí)間的觸發(fā)器揭北,基本上說的就是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)聽以下時(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à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 潜秋。主要是為了分隔開不同的 Source、Timer胎许、Observer峻呛,讓它們之間互不影響。
當(dāng)RunLoop運(yùn)行在一個(gè)Mode上時(shí)辜窑,是無法接受處理其他Mode上的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ī)制
對(duì)于RunLoop而言最核心的事情就是保證線程在沒有消息的時(shí)候休眠贺氓,在有消息時(shí)喚醒蔚叨,以提高程序性能。RunLoop這個(gè)機(jī)制是依靠系統(tǒng)內(nèi)核來完成的(蘋果操作系統(tǒng)核心組件Darwin中的Mach)。
RunLoop通過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)聽端口,端口有消息就會(huì)觸發(fā)回調(diào)榜苫;而source0护戳,要手動(dòng)標(biāo)記為待處理和手動(dòng)喚醒RunLoop(是執(zhí)行 source0 時(shí)需要手動(dòng)調(diào)用 CFRunLoopWakeUp 來喚醒 run loop,實(shí)際覺得好像大部分場(chǎng)景下其它事件都會(huì)導(dǎo)致 run loop 正常進(jìn)行著循環(huán)垂睬,只要 run loop 進(jìn)行循環(huán)則標(biāo)記為待處理的 source0 就能得到執(zhí)行媳荒,好像并不需要我們刻意的手動(dòng)調(diào)用 CFRunLoopWakeUp 來喚醒當(dāng)前的 run loop。)
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í)間還沒超時(shí),重啟RunLoop疚宇。進(jìn)入步驟2
10、通知觀察者RunLoop結(jié)束赏殃。
六敷待、一些runloop常見的問題和應(yīng)用
1、RunLoop與NSTimer
一個(gè)比較常見的問題:滑動(dòng)tableView時(shí)仁热,定時(shí)器還會(huì)生效嗎榜揖?
默認(rèn)情況下RunLoop運(yùn)行在kCFRunLoopDefaultMode
下,而當(dāng)滑動(dòng)tableView時(shí)抗蠢,RunLoop切換到UITrackingRunLoopMode
举哟,而Timer是在kCFRunLoopDefaultMode
下的,就無法接受處理Timer的事件迅矛。
怎么去解決這個(gè)問題呢妨猩?把Timer添加到UITrackingRunLoopMode
上并不能解決問題,因?yàn)檫@樣在默認(rèn)情況下就無法接受定時(shí)器事件了秽褒。
所以我們需要把Timer同時(shí)添加到UITrackingRunLoopMode
和kCFRunLoopDefaultMode
上壶硅。
那么如何把timer同時(shí)添加到多個(gè)mode上呢?就要用到NSRunLoopCommonModes
了
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Timer就被添加到多個(gè)mode上销斟,這樣即使RunLoop由kCFRunLoopDefaultMode
切換到UITrackingRunLoopMode
下庐椒,也不會(huì)影響接收Timer事件
2、RunLoop和線程
- 線程和RunLoop是一一對(duì)應(yīng)的,其映射關(guān)系是保存在一個(gè)全局的 Dictionary 里
- 自己創(chuàng)建的線程默認(rèn)是沒有開啟RunLoop的
怎么創(chuàng)建一個(gè)常駐線程蚂踊?
1约谈、為當(dāng)前線程開啟一個(gè)RunLoop(第一次調(diào)用 [NSRunLoop currentRunLoop]方法時(shí)實(shí)際是會(huì)先去創(chuàng)建一個(gè)RunLoop)
2、向當(dāng)前RunLoop中添加一個(gè)Port/Source等維持RunLoop的事件循環(huán)(如果RunLoop的mode中一個(gè)item都沒有犁钟,RunLoop會(huì)退出)
3棱诱、啟動(dòng)該RunLoop
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[[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方法并不會(huì)執(zhí)行涝动。
原因是如果是帶afterDelay的延時(shí)函數(shù)军俊,會(huì)在內(nèi)部創(chuàng)建一個(gè) NSTimer,然后添加到當(dāng)前線程的RunLoop中捧存。也就是如果當(dāng)前線程沒有開啟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都沒有镰官,RunLoop會(huì)退出提前。即在調(diào)用RunLoop的run方法后,由于其mode中沒有添加任何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ù)回來更新UI的時(shí)候不打斷用戶的滑動(dòng)操作拓哺?
當(dāng)我們?cè)谧诱?qǐng)求數(shù)據(jù)的同時(shí)滑動(dòng)瀏覽當(dāng)前頁面,如果數(shù)據(jù)請(qǐng)求成功要切回主線程更新UI脖母,那么就會(huì)影響當(dāng)前正在滑動(dòng)的體驗(yàn)士鸥。
我們就可以將更新UI事件放在主線程的NSDefaultRunLoopMode
上執(zhí)行即可,這樣就會(huì)等用戶不再滑動(dòng)頁面谆级,主線程RunLoop由UITrackingRunLoopMode
切換到NSDefaultRunLoopMode
時(shí)再去更新UI
[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
4烤礁、runloop 在監(jiān)控卡頓中的應(yīng)用
主線程卡頓監(jiān)控:這是業(yè)內(nèi)常用的一種檢測(cè)卡頓的方法,通過開辟一個(gè)子線程來監(jiān)控主線程的 RunLoop肥照,當(dāng)兩個(gè)狀態(tài)區(qū)域之間的耗時(shí)大于閾值時(shí)脚仔,就記為發(fā)生一次卡頓。美團(tuán)的移動(dòng)端性能監(jiān)控方案 Hertz 采用的就是這種方式
主線程卡頓監(jiān)控的實(shí)現(xiàn)思路:開辟一個(gè)子線程舆绎,然后實(shí)時(shí)計(jì)算 kCFRunLoopBeforeSources
和 kCFRunLoopAfterWaiting
兩個(gè)狀態(tài)區(qū)域之間的耗時(shí)是否超過某個(gè)閥值鲤脏,來斷定主線程的卡頓情況,可以將這個(gè)過程想象成操場(chǎng)上跑圈的運(yùn)動(dòng)員吕朵,我們會(huì)每隔一段時(shí)間間隔去判斷是否跑了一圈凑兰,如果發(fā)現(xiàn)在指定時(shí)間間隔沒有跑完一圈,則認(rèn)為在消息處理的過程中耗時(shí)太多边锁,視為主線程卡頓姑食,這時(shí)我們要保存應(yīng)用的上下文,即卡頓發(fā)生時(shí)程序的堆棧調(diào)用和運(yùn)行日志上傳茅坛。
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
MyClass *object = (__bridge MyClass*)info;
// 記錄狀態(tài)值
object->activity = activity;
// 發(fā)送信號(hào)
dispatch_semaphore_t semaphore = moniotr->semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver
{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 創(chuàng)建信號(hào)
semaphore = dispatch_semaphore_create(0);
// 在子線程監(jiān)控時(shí)長(zhǎng)
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 假定連續(xù)5次超時(shí)50ms認(rèn)為卡頓(當(dāng)然也包含了單次超時(shí)250ms)
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (st != 0)
{
if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
{
if (++timeoutCount < 5)
continue;
// 檢測(cè)到卡頓音半,進(jìn)行卡頓上報(bào)
}
}
timeoutCount = 0;
}
});
}
代碼中使用 timeoutCount 變量來覆蓋多次連續(xù)的小卡頓,當(dāng)累計(jì)次數(shù)超過5次贡蓖,也會(huì)進(jìn)入到卡頓邏輯曹鸠。當(dāng)檢測(cè)到了卡頓,下一步需要做的就是記錄卡頓的現(xiàn)場(chǎng)斥铺,即此時(shí)程序的堆棧調(diào)用彻桃,可以借助開源庫 PLCrashReporter 來實(shí)現(xiàn)