iOS 之如何利用 RunLoop 原理去監(jiān)控卡頓?
1. 前言
卡頓問(wèn)題,就是在主線程上無(wú)法響應(yīng)用戶交互的問(wèn)題。如果一個(gè) App 時(shí)不時(shí)地就給你卡一下,有 時(shí)還長(zhǎng)時(shí)間無(wú)響應(yīng),這時(shí)你還愿意繼續(xù)用它嗎?所以說(shuō),卡頓問(wèn)題對(duì) App 的傷害是巨大的,也是 我們必須要重點(diǎn)解決的一個(gè)問(wèn)題。
2. 卡頓原因
現(xiàn)在,我們先來(lái)看一下導(dǎo)致卡頓問(wèn)題的幾種原因:
- 復(fù)雜 UI 、圖文混排的繪制量過(guò)大;
- 在主線程上做網(wǎng)絡(luò)同步請(qǐng)求;
- 在主線程做大量的 IO 操作;
- 運(yùn)算量過(guò)大,CPU 持續(xù)高占用;
- 死鎖和主子線程搶鎖。
那么,我們?nèi)绾伪O(jiān)控到什么時(shí)候會(huì)出現(xiàn)卡頓呢?是要監(jiān)視FPS嗎?
FPS 是一秒顯示的幀數(shù),也就是一秒內(nèi)畫(huà)面變化數(shù)量。當(dāng)FPS達(dá)到60,說(shuō)明界面很流程,當(dāng)FPS低于24,頁(yè)面流暢度不是那么流暢,但是不能說(shuō)卡主了。
由此可見(jiàn),簡(jiǎn)單地通過(guò)監(jiān)視 FPS 是很難確定是否會(huì)出現(xiàn)卡頓問(wèn)題了,所以我就果斷棄了通過(guò)監(jiān)視 FPS 來(lái)監(jiān)控卡頓的方案。
那么,我們到底應(yīng)該使用什么方案來(lái)監(jiān)控卡頓呢?
3. 使用RunLoop來(lái)檢控卡頓
對(duì)于 iOS 開(kāi)發(fā)來(lái)說(shuō),監(jiān)控卡頓就是要去找到主線程上都做了哪些事兒。我們都知道,線程的消息 事件是依賴于 NSRunLoop 的,所以從 NSRunLoop 入手,就可以知道主線程上都調(diào)用了哪些方 法。我們通過(guò)監(jiān)聽(tīng) NSRunLoop 的狀態(tài),就能夠發(fā)現(xiàn)調(diào)用方法是否執(zhí)行時(shí)間過(guò)長(zhǎng),從而判斷出是 否會(huì)出現(xiàn)卡頓。
所以,我推薦的監(jiān)控卡頓的方案是:通過(guò)監(jiān)控 RunLoop 的狀態(tài)來(lái)判斷是否會(huì)出現(xiàn)卡頓。
3.1 Runloop
RunLoop是iOS開(kāi)發(fā)中的一個(gè)基礎(chǔ)概念,為了幫助你理解并用好這個(gè)對(duì)象,接下來(lái)我會(huì)先和你介紹一下它可以做哪些事兒,以及它為什么可以做成這些事兒。
RunLoop 這個(gè)對(duì)象,在 iOS 里由 CFRunLoop 實(shí)現(xiàn)。簡(jiǎn)單來(lái)說(shuō),RunLoop 是用來(lái)監(jiān)聽(tīng)輸入源,進(jìn) 行調(diào)度處理的。這里的輸入源可以是輸入設(shè)備、網(wǎng)絡(luò)、周期性或者延遲時(shí)間、異步回調(diào)。
RunLoop 會(huì)接收兩種類(lèi)型的輸入源:
- 一種是來(lái)自另一個(gè)線程或者來(lái)自不同應(yīng)用的異步消息;
- 另一 種是來(lái)自預(yù)訂時(shí)間或者重復(fù)間隔的同步事件。
RunLoop 的目的是,當(dāng)有事件要去處理時(shí)保持線程忙,當(dāng)沒(méi)有事件要處理時(shí)讓線程進(jìn)入休眠。所 以,了解 RunLoop 原理不光能夠運(yùn)用到監(jiān)控卡頓上,還可以提高用戶的交互體驗(yàn)。通過(guò)將那些 繁重而不緊急會(huì)大量占用 CPU 的任務(wù)(比如圖片加載),放到空閑的 RunLoop 模式里執(zhí)行,這 樣就可以避開(kāi)在 UITrackingRunLoopMode 這個(gè) RunLoop 模式時(shí)是執(zhí)行。
UITrackingRunLoopMode 是用戶進(jìn)行滾動(dòng)操作時(shí)會(huì)切換到的 RunLoop 模式,避免在這個(gè) RunLoop 模式執(zhí)行繁重的 CPU 任務(wù),就能避免影響用戶交互操作上體驗(yàn)。
接下來(lái),我就通過(guò) CFRunLoop 的源碼來(lái)跟你分享下 RunLoop 的原理吧。
3.2 RunLoop原理
其內(nèi)部代碼整理如下:
- /// 用DefaultMode啟動(dòng)
- void CFRunLoopRun(void) {
- CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
- }
- /// 用指定的Mode啟動(dòng),允許設(shè)置RunLoop超時(shí)時(shí)間
- int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
- return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
- }
- /// RunLoop的實(shí)現(xiàn)
- int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
- /// 首先根據(jù)modeName找到對(duì)應(yīng)mode
- CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
- /// 如果mode里沒(méi)有source/timer/observer, 直接返回。
- if (__CFRunLoopModeIsEmpty(currentMode)) return;
- /// 1. 通知 Observers: RunLoop 即將進(jìn)入 loop。
- __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
- /// 內(nèi)部函數(shù),進(jìn)入loop
- __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
- Boolean sourceHandledThisLoop = NO;
- int retVal = 0;
- do {
- /// 2. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)。
- __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
- /// 3. 通知 Observers: RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)。
- __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
- /// 執(zhí)行被加入的block
- __CFRunLoopDoBlocks(runloop, currentMode);
- /// 4. RunLoop 觸發(fā) Source0 (非port) 回調(diào)。
- sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
- /// 執(zhí)行被加入的block
- __CFRunLoopDoBlocks(runloop, currentMode);
- /// 5. 如果有 Source1 (基于port) 處于 ready 狀態(tài),直接處理這個(gè) Source1 然后跳轉(zhuǎn)去處理消息。
- if (__Source0DidDispatchPortLastTime) {
- Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
- if (hasMsg) goto handle_msg;
- }
- /// 通知 Observers: RunLoop 的線程即將進(jìn)入休眠(sleep)。
- if (!sourceHandledThisLoop) {
- __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
- }
- /// 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息。線程將進(jìn)入休眠, 直到被下面某一個(gè)事件喚醒。
- /// • 一個(gè)基于 port 的Source 的事件。
- /// • 一個(gè) Timer 到時(shí)間了
- /// • RunLoop 自身的超時(shí)時(shí)間到了
- /// • 被其他什么調(diào)用者手動(dòng)喚醒
- __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
- mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
- }
- /// 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了。
- __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
- /// 收到消息,處理消息。
- handle_msg:
- /// 9.1 如果一個(gè) Timer 到時(shí)間了,觸發(fā)這個(gè)Timer的回調(diào)。
- if (msg_is_timer) {
- __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
- }
- /// 9.2 如果有dispatch到main_queue的block,執(zhí)行block。
- else if (msg_is_dispatch) {
- __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
- }
- /// 9.3 如果一個(gè) Source1 (基于port) 發(fā)出事件了,處理這個(gè)事件
- else {
- CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
- sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
- if (sourceHandledThisLoop) {
- mach_msg(reply, MACH_SEND_MSG, reply);
- }
- }
- /// 執(zhí)行加入到Loop的block
- __CFRunLoopDoBlocks(runloop, currentMode);
- if (sourceHandledThisLoop && stopAfterHandle) {
- /// 進(jìn)入loop時(shí)參數(shù)說(shuō)處理完事件就返回。
- retVal = kCFRunLoopRunHandledSource;
- } else if (timeout) {
- /// 超出傳入?yún)?shù)標(biāo)記的超時(shí)時(shí)間了
- retVal = kCFRunLoopRunTimedOut;
- } else if (__CFRunLoopIsStopped(runloop)) {
- /// 被外部調(diào)用者強(qiáng)制停止了
- retVal = kCFRunLoopRunStopped;
- } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
- /// source/timer/observer一個(gè)都沒(méi)有了
- retVal = kCFRunLoopRunFinished;
- }
- /// 如果沒(méi)超時(shí),mode里沒(méi)空,loop也沒(méi)被停止,那繼續(xù)loop。
- } while (retVal == 0);
- }
- /// 10. 通知 Observers: RunLoop 即將退出。
- __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
- }
可以看到,實(shí)際上 RunLoop 就是這樣一個(gè)函數(shù),其內(nèi)部是一個(gè) do-while 循環(huán)。當(dāng)你調(diào)用 CFRunLoopRun() 時(shí),線程就會(huì)一直停留在這個(gè)循環(huán)里;直到超時(shí)或被手動(dòng)停止,該函數(shù)才會(huì)返回。
RunLoop內(nèi)部的邏輯圖:
RunLoop內(nèi)部原理.png
4. 如何檢測(cè)卡頓
4.1 首先知道RunLoop的六個(gè)狀態(tài)
- typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
- kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入Loop
- kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
- kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
- kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
- kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
- kCFRunLoopExit = (1UL << 7), // 即將退出Loop
- kCFRunLoopAllActivities // loop所有狀態(tài)改變
- };
要想監(jiān)聽(tīng)RunLoop,你就首先需要?jiǎng)?chuàng)建一個(gè) CFRunLoopObserverContext 觀察者,代碼如下:
- - (void)registerObserver {
- CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
- //創(chuàng)建Run loop observer對(duì)象
- //第一個(gè)參數(shù)用于分配observer對(duì)象的內(nèi)存
- //第二個(gè)參數(shù)用以設(shè)置observer所要關(guān)注的事件,詳見(jiàn)回調(diào)函數(shù)myRunLoopObserver中注釋
- //第三個(gè)參數(shù)用于標(biāo)識(shí)該observer是在第一次進(jìn)入run loop時(shí)執(zhí)行還是每次進(jìn)入run loop處理時(shí)均執(zhí)行
- //第四個(gè)參數(shù)用于設(shè)置該observer的優(yōu)先級(jí)
- //第五個(gè)參數(shù)用于設(shè)置該observer的回調(diào)函數(shù)
- //第六個(gè)參數(shù)用于設(shè)置該observer的運(yùn)行環(huán)境
- CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
- kCFRunLoopAllActivities,
- YES,
- 0,
- &runLoopObserverCallBack,
- &context);
- CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
- }
實(shí)時(shí)獲取變化的回調(diào)的方法:
- //每當(dāng)runloop狀態(tài)變化的觸發(fā)這個(gè)回調(diào)方法
- static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
- MyClass *object = (__bridge MyClass*)info;
- object->activity = activity;
- }
其中UI主要集中在
_CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION(source0)和CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION(source1)之前。
獲取kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting的狀態(tài)就可以知道是否有卡頓的情況。
4.2 檢測(cè)卡頓的思路
只需要另外再開(kāi)啟一個(gè)線程,實(shí)時(shí)計(jì)算這兩個(gè)狀態(tài)區(qū)域之間的耗時(shí)是否到達(dá)某個(gè)閥值,便能揪出這些性能殺手。
- 監(jiān)聽(tīng)runloop狀態(tài)變化回調(diào)方法
- // 就是runloop有一個(gè)狀態(tài)改變 就記錄一下
- static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
- BGPerformanceMonitor *monitor = (__bridge BGPerformanceMonitor*)info;
- // 記錄狀態(tài)值
- monitor->activity = activity;
- // 發(fā)送信號(hào)
- dispatch_semaphore_t semaphore = monitor->semaphore;
- long st = dispatch_semaphore_signal(semaphore);
- NSLog(@"dispatch_semaphore_signal:st=%ld,time:%@",st,[BGPerformanceMonitor getCurTime]);
- /* Run Loop Observer Activities */
- // typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
- // kCFRunLoopEntry = (1UL << 0), // 進(jìn)入RunLoop循環(huán)(這里其實(shí)還沒(méi)進(jìn)入)
- // kCFRunLoopBeforeTimers = (1UL << 1), // RunLoop 要處理timer了
- // kCFRunLoopBeforeSources = (1UL << 2), // RunLoop 要處理source了
- // kCFRunLoopBeforeWaiting = (1UL << 5), // RunLoop要休眠了
- // kCFRunLoopAfterWaiting = (1UL << 6), // RunLoop醒了
- // kCFRunLoopExit = (1UL << 7), // RunLoop退出(和kCFRunLoopEntry對(duì)應(yīng))
- // kCFRunLoopAllActivities = 0x0FFFFFFFU //RunLoop狀態(tài)變化
- // };
- if (activity == kCFRunLoopEntry) { // 即將進(jìn)入RunLoop
- NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopEntry");
- } else if (activity == kCFRunLoopBeforeTimers) { // 即將處理Timer
- NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopBeforeTimers");
- } else if (activity == kCFRunLoopBeforeSources) { // 即將處理Source
- NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopBeforeSources");
- } else if (activity == kCFRunLoopBeforeWaiting) { //即將進(jìn)入休眠
- NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopBeforeWaiting");
- } else if (activity == kCFRunLoopAfterWaiting) { // 剛從休眠中喚醒
- NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopAfterWaiting");
- } else if (activity == kCFRunLoopExit) { // 即將退出RunLoop
- NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopExit");
- } else if (activity == kCFRunLoopAllActivities) {
- NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopAllActivities");
- }
- }
- 開(kāi)啟runloop監(jiān)聽(tīng)
- // 開(kāi)始監(jiān)聽(tīng)
- - (void)startMonitor {
- if (observer) {
- return;
- }
- // 創(chuàng)建信號(hào)
- semaphore = dispatch_semaphore_create(0);
- NSLog(@"dispatch_semaphore_create:%@",[BGPerformanceMonitor getCurTime]);
- // 注冊(cè)RunLoop狀態(tài)觀察
- CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
- //創(chuàng)建Run loop observer對(duì)象
- //第一個(gè)參數(shù)用于分配observer對(duì)象的內(nèi)存
- //第二個(gè)參數(shù)用以設(shè)置observer所要關(guān)注的事件,詳見(jiàn)回調(diào)函數(shù)myRunLoopObserver中注釋
- //第三個(gè)參數(shù)用于標(biāo)識(shí)該observer是在第一次進(jìn)入run loop時(shí)執(zhí)行還是每次進(jìn)入run loop處理時(shí)均執(zhí)行
- //第四個(gè)參數(shù)用于設(shè)置該observer的優(yōu)先級(jí)
- //第五個(gè)參數(shù)用于設(shè)置該observer的回調(diào)函數(shù)
- //第六個(gè)參數(shù)用于設(shè)置該observer的運(yùn)行環(huán)境
- observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
- kCFRunLoopAllActivities,
- YES,
- 0,
- &runLoopObserverCallBack,
- &context);
- CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
- // 在子線程監(jiān)控時(shí)長(zhǎng)
- dispatch_async(dispatch_get_global_queue(0, 0), ^{
- while (YES) { // 有信號(hào)的話 就查詢當(dāng)前runloop的狀態(tài)
- // 假定連續(xù)5次超時(shí)50ms認(rèn)為卡頓(當(dāng)然也包含了單次超時(shí)250ms)
- // 因?yàn)橄旅?nbsp;runloop 狀態(tài)改變回調(diào)方法runLoopObserverCallBack中會(huì)將信號(hào)量遞增 1,所以每次 runloop 狀態(tài)改變后,下面的語(yǔ)句都會(huì)執(zhí)行一次
- // dispatch_semaphore_wait:Returns zero on success, or non-zero if the timeout occurred.
- long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
- NSLog(@"dispatch_semaphore_wait:st=%ld,time:%@",st,[self getCurTime]);
- if (st != 0) { // 信號(hào)量超時(shí)了 - 即 runloop 的狀態(tài)長(zhǎng)時(shí)間沒(méi)有發(fā)生變更,長(zhǎng)期處于某一個(gè)狀態(tài)下
- if (!observer) {
- timeoutCount = 0;
- semaphore = 0;
- activity = 0;
- return;
- }
- NSLog(@"st = %ld,activity = %lu,timeoutCount = %d,time:%@",st,activity,timeoutCount,[self getCurTime]);
- // kCFRunLoopBeforeSources - 即將處理source kCFRunLoopAfterWaiting - 剛從休眠中喚醒
- // 獲取kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting的狀態(tài)就可以知道是否有卡頓的情況。
- // kCFRunLoopBeforeSources:停留在這個(gè)狀態(tài),表示在做很多事情
- if (activity == kCFRunLoopBeforeSources || activity == kCFRunLoopAfterWaiting) { // 發(fā)生卡頓,記錄卡頓次數(shù)
- if (++timeoutCount < 5) {
- continue; // 不足 5 次,直接 continue 當(dāng)次循環(huán),不將timeoutCount置為0
- }
- // 收集Crash信息也可用于實(shí)時(shí)獲取各線程的調(diào)用堆棧
- PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
- PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
- NSData *data = [crashReporter generateLiveReport];
- PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
- NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter withTextFormat:PLCrashReportTextFormatiOS];
- NSLog(@"---------卡頓信息\n%@\n--------------",report);
- }
- }
- NSLog(@"dispatch_semaphore_wait timeoutCount = 0,time:%@",[self getCurTime]);
- timeoutCount = 0;
- }
- });
- }
記錄卡頓的函數(shù)調(diào)用
監(jiān)控到了卡頓現(xiàn)場(chǎng),當(dāng)然下一步便是記錄此時(shí)的函數(shù)調(diào)用信息,此處可以使用一個(gè)第三方Crash 收集組件 PLCrashReporter,它不僅可以收集 Crash 信息也可用于實(shí)時(shí)獲取各線程的調(diào)用堆棧,示例如下:
- PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD
- symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
- PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
- NSData *data = [crashReporter generateLiveReport];
- PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
- NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter
- withTextFormat:PLCrashReportTextFormatiOS];
- NSLog(@"------------\n%@\n------------", report);
當(dāng)檢測(cè)到卡頓時(shí),抓取堆棧信息,然后在客戶端做一些過(guò)濾處理,便可以上報(bào)到服務(wù)器,通過(guò)收集一定量的卡頓數(shù)據(jù)后經(jīng)過(guò)分析便能準(zhǔn)確定位需要優(yōu)化的邏輯,這個(gè)實(shí)時(shí)卡頓監(jiān)控就大功告成了!
5. 結(jié)尾
通過(guò) Runloop 來(lái)檢測(cè)卡頓,還是很有必要的。對(duì)提高 app 的用戶使用體驗(yàn)還是很有幫助的。畢竟卡頓是偶顯的不容易復(fù)現(xiàn)。所以檢測(cè)卡頓來(lái)來(lái)抓取堆棧信息,分析并解決卡頓,還是很有必要的。