戴銘(iOS開發(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),但是原文中戴銘老師直接否定了通過 監(jiān)控FPS 來判斷程序是否卡頓的方案菠剩,進(jìn)而提出使用 監(jiān)控主線程RunLoop的狀態(tài) 來判斷是否卡頓的方法易猫。

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

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

簡單來說准颓,RunLoop 的工作模式就是,當(dāng)有事件要處理時(shí)保持線程忙棺妓,當(dāng)沒有事件要處理時(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,也就是說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 工作過程:

工作過程大致總結(jié)為上圖的10個(gè)步驟:
1 通知Observers鸡挠,RunLoop要開始進(jìn)入loop了
2-3 進(jìn)入loop,開啟一個(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 被喚醒后就重新開始處理消息,重復(fù)2-3的過程
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)聽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甘耿,通過Observer監(jiān)控卡頓

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

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

如何檢查卡頓

這里我們從老師分享的源碼 截取關(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), ^{
        //子線程開啟一個(gè)持續(xù)的loop用來進(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é)

通過 RunLoop 的 Observer 監(jiān)控 主線程 中各個(gè)狀態(tài)的變化。如果 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting 這兩個(gè)狀態(tài)所停留的時(shí)間過長倾剿,我們便認(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)聽 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 通過 info 屬性,拿到當(dāng)前類
1.2 記錄當(dāng)前 Observers 的狀態(tài)坯癣,并賦值給成員變量 runLoopActivity
1.3 使用信號(hào)量 dispatch_semaphore_t 監(jiān)控 Observers 狀態(tài)間停留的時(shí)長涵妥。這里獲取當(dāng)前類聲明的 dispatch_semaphore_t 信號(hào)量屬性
1.4 激活信號(hào)量,通過 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)迸钔活,并通過信號(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)切換沒有發(fā)生阻塞,那么會(huì)及時(shí)發(fā)出信號(hào)量的激活信號(hào)脯倒,此時(shí) dispatch_semaphore_wait 方法的返回值為0实辑,不視為卡頓。反之各狀態(tài)耗時(shí)過長藻丢,沒有及時(shí)發(fā)出信號(hào)剪撬,dispatch_semaphore_wait 方法的返回值為非0,就視為發(fā)生卡頓悠反。

3 觸發(fā)卡頓的時(shí)間閾值
我們根據(jù) WatchDog 機(jī)制來設(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ā)生后,自然要解決問題旦委,那么如何獲取卡頓的堆棧信息呢奇徒?

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

// 獲取數(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)行之斯。
說回提升用戶體驗(yàn)的話題日杈,我覺得更重要的是從產(chǎn)品角度和產(chǎn)品交互出發(fā),卡頓監(jiān)控只是一項(xiàng)必做的基本功課佑刷。好的產(chǎn)品交互才是提升用戶體驗(yàn)的重頭戲莉擒。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市瘫絮,隨后出現(xiàn)的幾起案子涨冀,更是在濱河造成了極大的恐慌,老刑警劉巖麦萤,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鹿鳖,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡壮莹,警方通過查閱死者的電腦和手機(jī)翅帜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來命满,“玉大人涝滴,你說我怎么就攤上這事≈芗觯” “怎么了狭莱?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵僵娃,是天一觀的道長概作。 經(jīng)常有香客問我,道長默怨,這世上最難降的妖魔是什么讯榕? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮匙睹,結(jié)果婚禮上愚屁,老公的妹妹穿的比我還像新娘。我一直安慰自己痕檬,他們只是感情好霎槐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著梦谜,像睡著了一般丘跌。 火紅的嫁衣襯著肌膚如雪袭景。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天闭树,我揣著相機(jī)與錄音耸棒,去河邊找鬼。 笑死报辱,一個(gè)胖子當(dāng)著我的面吹牛与殃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播碍现,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼幅疼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了昼接?” 一聲冷哼從身側(cè)響起衣屏,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎辩棒,沒想到半個(gè)月后狼忱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡一睁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年钻弄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片者吁。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡窘俺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出复凳,到底是詐尸還是另有隱情瘤泪,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布育八,位于F島的核電站对途,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏髓棋。R本人自食惡果不足惜实檀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望按声。 院中可真熱鬧膳犹,春花似錦、人聲如沸签则。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽渐裂。三九已至豺旬,卻和暖如春余赢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背哈垢。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國打工妻柒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人耘分。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓举塔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親求泰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子央渣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

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