戴銘(iOS開(kāi)發(fā)課)讀書筆記:13章節(jié)-卡頓監(jiān)控

原文鏈接:如何利用 RunLoop 原理去監(jiān)控卡頓?


前言

一個(gè)App想要提升用戶體驗(yàn)最重要的就是 降低程序崩潰提升程序流暢度。前者在上一篇 崩潰監(jiān)控 中稍有介紹,而今天要看的就是如何監(jiān)控程序的卡頓,從而有目的性的優(yōu)化程序流暢度,提升用戶體驗(yàn)。

雖然達(dá)到程序60FPS穩(wěn)定運(yùn)行是我們的終極目標(biāo),但是原文中戴銘老師直接否定了通過(guò) 監(jiān)控FPS 來(lái)判斷程序是否卡頓的方案,進(jìn)而提出使用 監(jiān)控主線程RunLoop的狀態(tài) 來(lái)判斷是否卡頓的方法。

RunLoop監(jiān)控卡頓原理

1 卡頓情況
  • 復(fù)雜 UI、圖文混排的繪制量過(guò)大
  • 在主線程上做網(wǎng)絡(luò)同步請(qǐng)求
  • 在主線程做大量 IO 操作
  • 運(yùn)算量過(guò)大,CPU持續(xù)高占用
  • 死鎖或主子線程間搶鎖
2 RunLoop基礎(chǔ)概念

簡(jiǎn)單來(lái)說(shuō),RunLoop 的工作模式就是,當(dāng)有事件要處理時(shí)保持線程忙,當(dāng)沒(méi)有事件要處理時(shí)讓線程進(jìn)入休眠。

2.1 相關(guān)的類:
CFRunLoopRef    
CFRunLoopModeRef 
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
2.2 Mode:

一個(gè)RunLoop包含若干個(gè)Mode,每個(gè)Mode又包含若干個(gè)Source/Timer/Observer。

系統(tǒng)默認(rèn)注冊(cè)了5個(gè)Mode。每次調(diào)用RunLoop的主函數(shù)時(shí),只能指定其中一個(gè)Mode,也就是說(shuō)RunLoop中的Mode在不斷切換。

kCFRunLoopDefaultMode //App的默認(rèn)Mode,通常主線程是在這個(gè)Mode下運(yùn)行
UITrackingRunLoopMode //界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他 Mode 影響
UIInitializationRunLoopMode // 在剛啟動(dòng) App 時(shí)第進(jìn)入的第一個(gè) Mode,啟動(dòng)完成后就不再使用
GSEventReceiveRunLoopMode // 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到
kCFRunLoopCommonModes //這是一個(gè)占位用的Mode,不是一種真正的Mode
2.3 工作過(guò)程:

工作過(guò)程大致總結(jié)為上圖的10個(gè)步驟:
1 通知Observers,RunLoop要開(kāi)始進(jìn)入loop了
2-3 進(jìn)入loop,開(kāi)啟一個(gè) do while ?;罹€程。通知Observers,將要處理Timer回調(diào)和Source0回調(diào),接著執(zhí)行block

// 通知 Observers RunLoop 會(huì)觸發(fā) Timer 回調(diào)
if (currentMode->_observerMask & kCFRunLoopBeforeTimers)
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 通知 Observers RunLoop 會(huì)觸發(fā) Source0 回調(diào)
if (currentMode->_observerMask & kCFRunLoopBeforeSources)
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 執(zhí)行 block
__CFRunLoopDoBlocks(runloop, currentMode);

4-5 處理Source0回調(diào),如果這里有Source1是ready狀態(tài),就會(huì)跳轉(zhuǎn)handle_msg去處理消息

if (MACH_PORT_NULL != dispatchPort ) {
    Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
    if (hasMsg) goto handle_msg;
}

6 回調(diào)觸發(fā)后,通知Observers,該線程即將進(jìn)入休眠
7-8 進(jìn)入休眠后,如果出現(xiàn)下面四個(gè)事件時(shí)RunLoop會(huì)通知Observers,線程被喚醒了

  • 基于 port 的 Source 事件
  • Timer 時(shí)間到
  • RunLoop 超時(shí)
  • 被調(diào)用者喚醒

9 RunLoop 被喚醒后就重新開(kāi)始處理消息,重復(fù)2-3的過(guò)程
10 當(dāng)被外部強(qiáng)制停止或loop超時(shí),就不繼續(xù)下一個(gè)loop了,此時(shí)通知Observers,即將退出loop

if (sourceHandledThisLoop && stopAfterHandle) {
     // 事件已處理完
    retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
    // 超時(shí)
    retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
    // 外部調(diào)用者強(qiáng)制停止
    retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
    // mode 為空,RunLoop 結(jié)束
    retVal = kCFRunLoopRunFinished;
}
2.4 Observer,loop的六個(gè)狀態(tài)

觀察者,可以監(jiān)聽(tīng)RunLoop的狀態(tài)改變

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { 
  kCFRunLoopEntry = (1UL << 0), // 進(jìn)入 loop
  kCFRunLoopBeforeTimers = (1UL << 1), //即將處理  Timer 
  kCFRunLoopBeforeSources = (1UL << 2), //即將處理 Sources0
  kCFRunLoopBeforeWaiting = (1UL << 5), //即將進(jìn)入休眠 
  kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中喚醒 
  kCFRunLoopExit = (1UL << 7), // 退出 loop 
  kCFRunLoopAllActivities = 0x0FFFFFFFU //所有狀態(tài)改變
};
3 RunLoop,通過(guò)Observer監(jiān)控卡頓

我們通過(guò)RunLoop的工作流程可以知道,如果在 loop進(jìn)入睡眠前執(zhí)行方法時(shí)間過(guò)長(zhǎng)(過(guò)程2-5) 或者 線程喚醒時(shí)接收消息時(shí)間過(guò)長(zhǎng)(過(guò)程8)而無(wú)法處理下一個(gè)事件,我們就可以認(rèn)為線程受阻而出現(xiàn)了卡頓。

上面兩種情況,我們可以通過(guò)監(jiān)聽(tīng)RunLoop的 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting 這兩個(gè)狀態(tài)所停留的時(shí)長(zhǎng)來(lái)判斷。

如何檢查卡頓

這里我們從老師分享的源碼 截取關(guān)鍵部分 進(jìn)行分析和學(xué)習(xí)。

#import "SMLagMonitor.h"
#import "SMCallStack.h"
#import "SMCPUMonitor.h"

@interface SMLagMonitor() {
    int timeoutCount;
    CFRunLoopObserverRef runLoopObserver;
    @public
    dispatch_semaphore_t dispatchSemaphore;
    CFRunLoopActivity runLoopActivity;
}
@end

@implementation SMLagMonitor

#pragma mark - Interface
+ (instancetype)shareInstance {
    static id instance = nil;
    static dispatch_once_t dispatchOnce;
    dispatch_once(&dispatchOnce, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

- (void)beginMonitor {
    //監(jiān)測(cè)卡頓
    if (runLoopObserver) {
        return;
    }
    dispatchSemaphore = dispatch_semaphore_create(0); //Dispatch Semaphore保證同步
    //創(chuàng)建一個(gè)觀察者
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                              kCFRunLoopAllActivities,
                                              YES,
                                              0,
                                              &runLoopObserverCallBack,
                                              &context);
    //將觀察者添加到主線程runloop的common模式下的觀察中
    CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
    
    //創(chuàng)建子線程監(jiān)控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //子線程開(kāi)啟一個(gè)持續(xù)的loop用來(lái)進(jìn)行監(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;
                }
                //兩個(gè)runloop的狀態(tài),BeforeSources和AfterWaiting這兩個(gè)狀態(tài)區(qū)間時(shí)間能夠檢測(cè)到是否卡頓
                if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
                    // 出現(xiàn)異常情況
                    NSLog(@"monitor trigger");
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                        // 異步提交/上傳錯(cuò)誤的堆棧信息
                    });
                } //end activity
            }// end semaphore wait
            timeoutCount = 0;
        }// end while
    });
    
}

- (void)endMonitor {
    if (!runLoopObserver) {
        return;
    }
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
    CFRelease(runLoopObserver);
    runLoopObserver = NULL;
}

#pragma mark - Private
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info;
    lagMonitor->runLoopActivity = activity;
    
    dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
    dispatch_semaphore_signal(semaphore);
}
@end
思路總結(jié)

通過(guò) RunLoop 的 Observer 監(jiān)控 主線程 中各個(gè)狀態(tài)的變化。如果 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting 這兩個(gè)狀態(tài)所停留的時(shí)間過(guò)長(zhǎng),我們便認(rèn)定為發(fā)生了一次主線程卡頓。

具體做法

1 我們需要?jiǎng)?chuàng)建一個(gè) CFRunLoopObserverContext 觀察者,且創(chuàng)建一個(gè) Observer,并監(jiān)控主線程狀態(tài)的變化

CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);
//將觀察者添加到主線程runloop的common模式下的觀察中
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);

這個(gè)Observer會(huì)監(jiān)聽(tīng) kCFRunLoopAllActivities(所有狀態(tài)改變),并在狀態(tài)改變時(shí)執(zhí)行 runLoopObserverCallBack 中的代碼。

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info;
    lagMonitor->runLoopActivity = activity;
    
    dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
    dispatch_semaphore_signal(semaphore);
}

這個(gè)閉包中執(zhí)行了4行代碼:
1.1 通過(guò) info 屬性,拿到當(dāng)前類
1.2 記錄當(dāng)前 Observers 的狀態(tài),并賦值給成員變量 runLoopActivity
1.3 使用信號(hào)量 dispatch_semaphore_t 監(jiān)控 Observers 狀態(tài)間停留的時(shí)長(zhǎng)。這里獲取當(dāng)前類聲明的 dispatch_semaphore_t 信號(hào)量屬性
1.4 激活信號(hào)量,通過(guò) dispatch_semaphore_signal() 方法使正在等待的信號(hào)量繼續(xù)執(zhí)行

對(duì)應(yīng)之前創(chuàng)建 dispatch_semaphore_t 對(duì)象的的代碼是:

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

2 創(chuàng)建一個(gè)子線程,使用while循環(huán)保活,并通過(guò)信號(hào)量阻塞該線程

long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3*NSEC_PER_MSEC));
if (semaphoreWait != 0) {
  // Returns zero on success, or non-zero if the timeout occurred.
}

dispatch_semaphore_wait 這個(gè)方法會(huì)阻塞當(dāng)前線程一段時(shí)間,如果 在阻塞時(shí)間內(nèi)收到激活信號(hào) 或者 阻塞時(shí)間超時(shí),代碼會(huì)繼續(xù)執(zhí)行,如果超時(shí),該方法的返回值為 非0

對(duì)應(yīng)前面的閉包中的代碼,如果各狀態(tài)切換沒(méi)有發(fā)生阻塞,那么會(huì)及時(shí)發(fā)出信號(hào)量的激活信號(hào),此時(shí) dispatch_semaphore_wait 方法的返回值為0,不視為卡頓。反之各狀態(tài)耗時(shí)過(guò)長(zhǎng),沒(méi)有及時(shí)發(fā)出信號(hào),dispatch_semaphore_wait 方法的返回值為非0,就視為發(fā)生卡頓。

3 觸發(fā)卡頓的時(shí)間閾值
我們根據(jù) WatchDog 機(jī)制來(lái)設(shè)置。

  • 啟動(dòng) 20s
  • 恢復(fù) 10s
  • 掛起 10s
  • 退出 6s
  • 后臺(tái) 3min(iOS7之前每次申請(qǐng)10min,之后改為3min,可以連續(xù)申請(qǐng),最多申請(qǐng)到10min)

總的原則就是,要小于 WatchDog 的限制時(shí)間,3s僅做參考值。

4 獲取卡頓的方法堆棧信息
監(jiān)控到卡頓發(fā)生后,自然要解決問(wèn)題,那么如何獲取卡頓的堆棧信息呢?

原文中推薦的是直接用 plcrashreporter 能夠定位到問(wèn)題代碼的具體位置,而且性能消耗也不大。
具體使用的代碼:

// 獲取數(shù)據(jù)
NSData *lagData = [[[PLCrashReporter alloc] initWithConfiguration:[[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]] generateLiveReport];
// 轉(zhuǎn)換成 PLCrashReport 對(duì)象
PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];
// 進(jìn)行字符串格式化處理
NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];
// 將字符串上傳服務(wù)器
NSLog(@"lag happen, detail below: \n %@",lagReportString);

最后

現(xiàn)在,我們可以監(jiān)控卡頓,并且獲取發(fā)生卡頓的方法信息了。
這里涉及的知識(shí)主要包括了 RunLoop 和 信號(hào)量(線程鎖知識(shí))。當(dāng)然也只是皮毛,更多是需要我們自己去實(shí)戰(zhàn)和應(yīng)用。
比起事后排查和改進(jìn),我們更應(yīng)該養(yǎng)成良好且正確的代碼習(xí)慣,通常情況下,設(shè)備的性能都足以支撐正確程序的流暢運(yùn)行。
說(shuō)回提升用戶體驗(yàn)的話題,我覺(jué)得更重要的是從產(chǎn)品角度和產(chǎn)品交互出發(fā),卡頓監(jiān)控只是一項(xiàng)必做的基本功課。好的產(chǎn)品交互才是提升用戶體驗(yàn)的重頭戲。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • Runloop是iOS和OSX開(kāi)發(fā)中非?;A(chǔ)的一個(gè)概念,從概念開(kāi)始學(xué)習(xí)。 RunLoop的概念 -般說(shuō),一個(gè)線程一...
    小貓仔閱讀 1,105評(píng)論 0 1
  • 轉(zhuǎn)載:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling閱讀 1,547評(píng)論 0 13
  • http://www.cocoachina.com/ios/20150601/11970.html RunLoop...
    紫色冰雨閱讀 944評(píng)論 0 3
  • 轉(zhuǎn)自bireme,原地址:https://blog.ibireme.com/2015/05/18/runloop/...
    乜_啊_閱讀 1,673評(píng)論 0 5
  • 這本是主打短篇夾雜兩篇中短篇(可以很容易的從頁(yè)碼里分辨出來(lái))的小集,情節(jié)相比長(zhǎng)篇自然不需要多少曲折迂回,登臺(tái)表演的...
    Chihiro_Jia閱讀 373評(píng)論 0 1

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