iOS性能優(yōu)化-RunLoop卡頓監(jiān)控

卡頓主要表現(xiàn)為主線程卡死讨盒,不響應(yīng)用戶動作或者響應(yīng)很慢解取,這種體驗(yàn)很差,會讓用戶對產(chǎn)品的認(rèn)可度急速下滑返顺,如果不及時優(yōu)化禀苦,最終會導(dǎo)致用戶流失。

那么遂鹊,哪些情況會導(dǎo)致主線程卡頓呢振乏?大體有如下幾個方面:

  • 很復(fù)雜的 UI 、圖文混排的繪制量很大秉扑;
  • 主線程進(jìn)行網(wǎng)絡(luò)同步請求慧邮;
  • 主線程上做大量的 IO 操作;
  • 運(yùn)算量過大邻储,CPU 持續(xù)高占用赋咽;
  • 死鎖和主子線程搶鎖旧噪。

檢測方案

為了優(yōu)化卡頓吨娜,我們需要準(zhǔn)確的知道哪里發(fā)生了卡頓,然后才能有針對性的進(jìn)行優(yōu)化淘钟,所以在開始優(yōu)化之前我們需要去監(jiān)控卡頓發(fā)生的地方宦赠。那么問題來了,怎么監(jiān)控卡頓米母?

檢測 FPS 變化幅度是一種方案勾扭,但是并不推薦,原因我引用戴銘大佬在如何利用 RunLoop 原理去監(jiān)控卡頓铁瞒?一文中的描述:”FPS 是一秒顯示的幀數(shù)妙色,也就是一秒內(nèi)畫面變化數(shù)量。如果按照動畫片來說慧耍,動畫片的 FPS 就是 24身辨,是達(dá)不到 60 滿幀的。也就是說芍碧,對于動畫片來說煌珊,24 幀時雖然沒有 60 幀時流暢,但也已經(jīng)是連貫的了泌豆,所以并不能說 24 幀時就算是卡住了定庵。“

另一種推薦的方案就是 RunLoop。為什么Runloop可以做到卡頓監(jiān)控蔬浙?我們知道程序中的任務(wù)都是在線程中執(zhí)行猪落,而線程依賴于 RunLoop,并且RunLoop總是在相應(yīng)的狀態(tài)下執(zhí)行任務(wù)畴博,執(zhí)行完成以后會切換到下一個狀態(tài)许布,如果在一個狀態(tài)下執(zhí)行時間過長導(dǎo)致無法進(jìn)入下一個狀態(tài)就可以認(rèn)為發(fā)生了卡頓,所以可以根據(jù)主線程 RunLoop 的狀態(tài)變化檢測任務(wù)執(zhí)行時間是否太長绎晃。至于多長時間算作卡頓可以依據(jù)自己的需要來設(shè)置蜜唾,一般情況下可以設(shè)置1秒鐘作為閥值。

RunLoop 的狀態(tài)如下:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 進(jìn)入Runloop
    kCFRunLoopBeforeTimers = (1UL << 1), // 處理Timer事件
    kCFRunLoopBeforeSources = (1UL << 2), // 處理Source事件
    kCFRunLoopBeforeWaiting = (1UL << 5), // 進(jìn)入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), // 喚醒
    kCFRunLoopExit = (1UL << 7), // 退出Runloop
    kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有狀態(tài)
};

RunLoop 的執(zhí)行流程:

image

在一次循環(huán)中庶艾,Timer事件袁余、Source事件、喚醒后事件如果處理時間過長都可以認(rèn)為卡頓了咱揍;當(dāng)然還有一種休眠前的事件颖榜,但是監(jiān)控這個事件時需要特別小心,因?yàn)椴荒馨研菝叩臅r間算作是卡頓的煤裙。

具體實(shí)現(xiàn)

大體的思路有了掩完,那怎么來實(shí)現(xiàn)呢?要監(jiān)控 RunLoop 事件硼砰,首先需要一個觀察者:

CFRunLoopObserverContext context = {
    0, // 直接傳0就好
    (__bridge void*)self, // 對應(yīng)回調(diào)中地方 void *info 參數(shù)
    &CFRetain, // 內(nèi)存管理方案
    &CFRelease, // 內(nèi)存管理方案
    NULL
};

observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runloopObserverCallback, &context);

觀察主線程:

CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);

在回調(diào)函數(shù)中且蓬,需要記錄下當(dāng)前的模式以便于后面檢測任務(wù)的處理:

static void runloopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    [LagMonitor shared]->currentActivity = activity;
    
    dispatch_semaphore_t sema = [LagMonitor shared]->semaphore;
    dispatch_semaphore_signal(sema);
}

然后,不能在主線程中進(jìn)行觀察任務(wù)题翰,因?yàn)槲覀冇^測的是主線程本身的任務(wù)恶阴,把觀察后的處理任務(wù)也加到主線程會使得主線程任務(wù)不純粹,影響檢測結(jié)果的準(zhǔn)確性豹障。所以冯事,我們在子線程中處理檢測任務(wù),相應(yīng)的代碼和釋義如下:

// 在子線程中監(jiān)控卡頓
semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 開啟持續(xù)的loop來監(jiān)控
    while ([LagMonitor shared]->isMonitoring) {
        if ([LagMonitor shared]->currentActivity == kCFRunLoopBeforeWaiting)
        {
          // 處理休眠前事件觀測
            __block BOOL timeOut = YES;
            dispatch_async(dispatch_get_main_queue(), ^{
                timeOut = NO; // timeOut任務(wù)
            });
            [NSThread sleepForTimeInterval:WAIT_TIME];
            // WAIT_TIME 時間后,如果 timeOut任務(wù) 任未執(zhí)行, 則認(rèn)為主線程前面的任務(wù)執(zhí)行時間過長導(dǎo)致卡頓
            if (timeOut) {
                [LXDBacktraceLogger lxd_logMain]; // 輸出堆棧信息
            }
        }
        else
        {
            // 處理 Timer,Source,喚醒后事件
            // 同步等待時間內(nèi),接收到信號result=0, 超時則繼續(xù)往下執(zhí)行并且result!=0
            long result = dispatch_semaphore_wait([LagMonitor shared]->semaphore, dispatch_time(DISPATCH_TIME_NOW, OUT_TIME));
            if (result != 0) { // 超時
                if (![LagMonitor shared]->observer) {
                    [[LagMonitor shared] endMonitor];
                    continue;
                }
                if ([LagMonitor shared]->currentActivity == kCFRunLoopBeforeSources ||
                    [LagMonitor shared]->currentActivity == kCFRunLoopAfterWaiting  ||
                    [LagMonitor shared]->currentActivity == kCFRunLoopBeforeTimers) {

                    [LXDBacktraceLogger lxd_logMain]; // 輸出堆棧信息
                }
            }
        }
    }
});

項(xiàng)目的全部代碼都在 這里 血公,其中 [LXDBacktraceLogger lxd_logMain] 使用了 LXDAppFluecyMonitor 中的開源代碼輸出堆棧信息昵仅。

檢測效果

我們運(yùn)行看一下效果,首先調(diào)用

[[LagMonitor shared] beginMonitor];

查看日志輸出:

runloop卡頓監(jiān)控[45103:2594859] touchesBegan

runloop卡頓監(jiān)控[45103:2595184] 主線程卡頓 Backtrace of Thread 771:
======================================================================================
libsystem_kernel.dylib         0x7fff5e703756 __semwait_signal + 10
Foundation                     0x7fff2085188c +[NSThread sleepForTimeInterval:] + 170
runloop??°è°?á??êé?            0x107bbde06 -[ViewController touchesBegan:withEvent:] + 118
UIKitCore                      0x7fff246a8b63 forwardTouchMethod + 321
UIKitCore                      0x7fff246a8a11 -[UIResponder touchesBegan:withEvent:] + 49
UIKitCore                      0x7fff246b7ad1 -[UIWindow _sendTouchesForEvent:] + 622
UIKitCore                      0x7fff246b9be3 -[UIWindow sendEvent:] + 4774
UIKitCore                      0x7fff246938f6 -[UIApplication sendEvent:] + 633
UIKitCore                      0x7fff2472439c __processEventQueue + 13895
UIKitCore                      0x7fff2471ad0f __eventFetcherSourceCallback + 104
CoreFoundation                 0x7fff2038c37a __CFRUNLOOP_IS_CALLING_OUT_TO_A_SO

日志顯示累魔,在 -[ViewController touchesBegan:withEvent:] 中有+[NSThread sleepForTimeInterval:] 發(fā)生了卡頓摔笤,回到項(xiàng)目中檢查代碼:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesBegan");   
    [NSThread sleepForTimeInterval:2];
}

與日志符合,這里確實(shí)發(fā)生了卡頓薛夜,就可以有針對性的進(jìn)行優(yōu)化籍茧。

項(xiàng)目地址:runloop卡頓監(jiān)控

參考資料

深入理解RunLoop,ibireme

如何利用 RunLoop 原理去監(jiān)控卡頓梯澜?寞冯,戴銘

LXDAppFluecyMonitor

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末渴析,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子吮龄,更是在濱河造成了極大的恐慌俭茧,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,080評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件漓帚,死亡現(xiàn)場離奇詭異母债,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)尝抖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,422評論 3 385
  • 文/潘曉璐 我一進(jìn)店門毡们,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人昧辽,你說我怎么就攤上這事衙熔。” “怎么了搅荞?”我有些...
    開封第一講書人閱讀 157,630評論 0 348
  • 文/不壞的土叔 我叫張陵红氯,是天一觀的道長。 經(jīng)常有香客問我咕痛,道長痢甘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,554評論 1 284
  • 正文 為了忘掉前任茉贡,我火速辦了婚禮塞栅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘块仆。我一直安慰自己构蹬,他們只是感情好王暗,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,662評論 6 386
  • 文/花漫 我一把揭開白布悔据。 她就那樣靜靜地躺著,像睡著了一般俗壹。 火紅的嫁衣襯著肌膚如雪科汗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,856評論 1 290
  • 那天绷雏,我揣著相機(jī)與錄音头滔,去河邊找鬼。 笑死涎显,一個胖子當(dāng)著我的面吹牛坤检,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播期吓,決...
    沈念sama閱讀 39,014評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼早歇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起箭跳,我...
    開封第一講書人閱讀 37,752評論 0 268
  • 序言:老撾萬榮一對情侶失蹤晨另,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后谱姓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體借尿,經(jīng)...
    沈念sama閱讀 44,212評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,541評論 2 327
  • 正文 我和宋清朗相戀三年屉来,在試婚紗的時候發(fā)現(xiàn)自己被綠了路翻。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,687評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡茄靠,死狀恐怖帚桩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情嘹黔,我是刑警寧澤账嚎,帶...
    沈念sama閱讀 34,347評論 4 331
  • 正文 年R本政府宣布,位于F島的核電站儡蔓,受9級特大地震影響郭蕉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜喂江,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,973評論 3 315
  • 文/蒙蒙 一召锈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧获询,春花似錦涨岁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,777評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至尝哆,卻和暖如春秉撇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背秋泄。 一陣腳步聲響...
    開封第一講書人閱讀 32,006評論 1 266
  • 我被黑心中介騙來泰國打工琐馆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人恒序。 一個月前我還...
    沈念sama閱讀 46,406評論 2 360
  • 正文 我出身青樓瘦麸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親歧胁。 傳聞我的和親對象是個殘疾皇子滋饲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,576評論 2 349

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