利用 RunLoop 監(jiān)控卡頓

導致卡頓問題的幾種原因:

  • 復雜 UI 、圖文混排的繪制量過大;
  • 在主線程上做網(wǎng)絡同步請求;
  • 在主線程做大量的 IO 操作;
  • 運算量過大,CPU 持續(xù)高占用;
  • 死鎖和主子線程搶鎖。

RunLoop 原理

RunLoop 在 iOS 里由 CFRunLoop 實現(xiàn)。簡單來說,RunLoop 是用來監(jiān)聽輸入源,進行調(diào)度處理的。

這里的輸入源可以是輸入設備、網(wǎng)絡、周期性或者延遲時間、異步回調(diào)。

RunLoop 會接收兩種類型的輸入源:一種是來自另一個線程或者來自不同應用的異步消息;另一種是來自預訂時間或者重復間隔的同步事件。

RunLoop 的目的是,當有事件要去處理時保持線程忙,當沒有事件要處理時讓線程進入休眠。所以,RunLoop 不光能夠運用到監(jiān)控卡頓上,還可以提高用戶的交互體驗。通過將那些繁重而不緊急會大量占用 CPU 的任務(比如圖片加載),放到空閑的 RunLoop 模式里執(zhí)行,就可以避開在 UITrackingRunLoopMode 這個 RunLoop 模式時執(zhí)行

RunLoop 執(zhí)行流程

在RunLoop運行的整個過程中,loop 的狀態(tài)包括 6 個,

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry , // 進入 loop
    kCFRunLoopBeforeTimers , // 觸發(fā) Timer 回調(diào)
    kCFRunLoopBeforeSources , // 觸發(fā) Source0 回調(diào)
    kCFRunLoopBeforeWaiting , // 等待 mach_port 消息
    kCFRunLoopAfterWaiting ), // 接收 mach_port 消息
    kCFRunLoopExit , // 退出 loop
    kCFRunLoopAllActivities  // loop 所有狀態(tài)改變
}
image.png
  • 通知 observers:RunLoop 要開始進入 loop 了。緊接著就進入 loop

  • 開啟一個 do while 來保活線程。通知 Observers:RunLoop 會觸發(fā) Timer 回調(diào)、Source0 回調(diào),接著執(zhí)行加入的 block。
    觸發(fā) Source0 回調(diào),如果有 Source1 是 ready 狀態(tài)的話,通知 Observers:結束休眠 , 跳轉到 handle_msg 去處理消息。

  • 回調(diào)觸發(fā)后,通知 Observers:RunLoop 的線程將進入休眠(sleep)狀態(tài)。

  • 進入休眠后,會等待 mach_port 的消息,以再次喚醒。只有在下面四個事件出現(xiàn)時才會被再次喚醒:
    基于 port 的 Source 事件;
    Timer 時間到;
    RunLoop 超時;
    被調(diào)用者喚醒。

  • 喚醒時通知 Observer:RunLoop 的線程剛剛被喚醒了。

  • RunLoop 被喚醒后就要開始處理消息了:
    如果是 Timer 時間到的話,就觸發(fā) Timer 的回調(diào);
    如果是 dispatch 的話,就執(zhí)行 block;
    如果是 source1 事件的話,就處理這個事件。
    消息執(zhí)行完后,就執(zhí)行加到 loop 里的 block。

  • 根據(jù)當前 RunLoop 的狀態(tài)來判斷是否需要走下一個 loop。當被外部強制停止或 loop 超時時,就不繼續(xù)下一個 loop 了,否則繼續(xù)下一個 loop 。

如果 RunLoop 的線程,進入睡眠前方法的執(zhí)行時間過長而導致無法進入睡眠,或者線程喚醒后接收消息時間過長而無法進入下一步的話,就可以認為是線程受阻了。如果這個線程是主線程的話,表現(xiàn)出來的就是出現(xiàn)了卡頓。

要利用 RunLoop 原理來監(jiān)控卡頓的話,要關注兩個階段。分別是 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting ,就是要觸發(fā) Source0 回調(diào)和接收 mach_port 消息兩個狀態(tài)。

如何檢查卡頓

創(chuàng)建 CFRunLoopObserverContext 觀察者
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);
// 添加觀察者
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);

觀察RunLoop 的 common 模式

將創(chuàng)建好的觀察者 runLoopObserver 添加到主線程 RunLoop 的 common 模式下觀察。
然后,創(chuàng)建一個持續(xù)的子線程專門用來監(jiān)控主線程的 RunLoop 狀態(tài)。
一旦發(fā)現(xiàn)進入睡眠前的 kCFRunLoopBeforeSources 狀態(tài),或者喚醒后的狀態(tài) kCFRunLoopAfterWaiting,在設置的時間閾值內(nèi)一直沒有變化,即可判定為卡頓。接下來,我們就可以 dump 出堆棧的信息,從而進一步分析出具體是哪個方法的執(zhí)行時間過長。

    dispatchSemaphore = dispatch_semaphore_create(0); //Dispatch Semaphore保證同步

    //創(chuàng)建子線程監(jiān)控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //子線程開啟一個持續(xù)的loop用來進行監(jiān)控
        while (YES) {
            long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3*NSEC_PER_MSEC));
            if (semaphoreWait != 0) {
                if (!runLoopObserver) {
                    timeoutCount = 0;
                    dispatchSemaphore = 0;
                    runLoopActivity = 0;
                    return;
                }
                //兩個runloop的狀態(tài),BeforeSources和AfterWaiting這兩個狀態(tài)區(qū)間時間能夠檢測到是否卡頓
                if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
                    //出現(xiàn)三次出結果
//                    if (++timeoutCount < 3) {
//                        continue;
//                    }
                    NSLog(@"monitor trigger");
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//                        [SMCallStack callStackWithType:SMCallStackTypeAll];
                    });
                } //end activity
            }// end semaphore wait
            timeoutCount = 0;
        }// end while
    });

收到beforesource通知發(fā)出后開始處理事件,處理完之后狀態(tài)會改變成beforewaiting。
收到afterwaiting也會開始處理事件,處理完之后變成beforetimer。
也就是說如果長時間停留在beforesource和afterwaiting狀態(tài),那么就發(fā)生了卡頓。

代碼中觸發(fā)卡頓的時間閾值 ,設置成了 3 秒。這個 3 秒的閾值合理?我們可以根據(jù) WatchDog 機制來設置。WatchDog 在不同狀態(tài)下設置的不同時間,如下所示:

  • 啟動(Launch):20s;
  • 恢復(Resume):10s;
  • 掛起(Suspend):10s;
  • 退出(Quit):6s;
  • 后臺(Background):3min(在 iOS 7 之前,每次申請 10min; 之后改為每次申請 3min,可連續(xù)申請,最多申請到 10min)。

如何獲取卡頓的方法堆棧信息

子線程監(jiān)控發(fā)現(xiàn)卡頓后,還需要記錄當前出現(xiàn)卡頓的方法堆棧信息,并適時推送到服務端供開發(fā)者分析,從而解決卡頓問題。

直接調(diào)用系統(tǒng)函數(shù)獲取堆棧

這種方法的優(yōu)點在于,性能消耗小。但是,它只能夠獲取簡單的信息,也沒有辦法配合 dSYM 來獲取具體是哪行代碼出了問題,而且能夠獲取的信息類型也有限。
但因為性能比較好,所以適用于觀察大盤統(tǒng)計卡頓情況,而不是想要找到卡頓原因的場景。

#include <libkern/OSAtomic.h>
#include <execinfo.h>

//獲取函數(shù)堆棧信息
+ (NSArray *)backtrace {
    void* callstack[128];
    int frames = backtrace(callstack, 128);//用于獲取當前線程的函數(shù)調(diào)用堆棧,返回實際獲取的指針個數(shù)
    char **strs = backtrace_symbols(callstack, frames);//從backtrace函數(shù)獲取的信息轉化為一個字符串數(shù)組
    int i;
    NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
    for (i = 0;
     i < backtrace.count;
     i++)  {
        [backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
    }
    free(strs);
    return backtrace;
}
三方庫

直接用 PLCrashReporter這個開源的第三方庫來獲取堆棧信息。這種方法的特點是,能夠定位到問題代碼的具體位置,而且性能消耗也不大。

// 獲取數(shù)據(jù)
NSData *lagData = [[[PLCrashReporter alloc]
                                          initWithConfiguration:[[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]] generateLiveReport];
// 轉換成 PLCrashReport 對象
PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];
// 進行字符串格式化處理
NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];
//將字符串上傳服務器
NSLog(@"lag happen, detail below: \n %@",lagReportString);
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容