質(zhì)量監(jiān)控-卡頓檢測

原文鏈接

不管是應(yīng)用秒變幻燈片,還是啟動(dòng)過久被殺,基本都是開發(fā)者必經(jīng)的體驗(yàn)。就像沒人希望堵車一樣,卡頓永遠(yuǎn)是不受用戶歡迎的,所以如何發(fā)現(xiàn)卡頓是開發(fā)者需要直面的難題。雖然導(dǎo)致卡頓的原因有很多,但卡頓的表現(xiàn)總是大同小異。如果把卡頓當(dāng)做病癥看待,兩者分別對(duì)應(yīng)所謂的本與標(biāo)。要檢測卡頓,無論是標(biāo)或本都可以下手,但都需要深入的學(xué)習(xí)

instruments與性能

在開發(fā)階段,使用內(nèi)置的性能工具instruments來檢測性能問題是最佳的選擇。與應(yīng)用運(yùn)行性能關(guān)聯(lián)最緊密的兩個(gè)硬件CPUGPU,前者用于執(zhí)行程序指令,針對(duì)代碼的處理邏輯;后者用于大量計(jì)算,針對(duì)圖像信息的渲染。正常情況下,CPU會(huì)周期性的提交要渲染的圖像信息給GPU處理,保證視圖的更新。一旦其中之一響應(yīng)不過來,就會(huì)表現(xiàn)為卡頓。因此多數(shù)情況下用到的工具是檢測GPU負(fù)載的Core Animation,以及檢測CPU處理效率的Time Profiler

由于CPU提交圖像信息是在主線程執(zhí)行的,會(huì)影響到CPU性能的誘因包括以下:

  1. 發(fā)生在主線程的I/O任務(wù)
  2. 過多的線程搶占CPU資源
  3. 溫度過高導(dǎo)致的CPU降頻

而影響GPU的因素較為客觀,難以針對(duì)做代碼上的優(yōu)化,包括:

  1. 顯存頻率
  2. 渲染算法
  3. 大計(jì)算量

本文旨在介紹如何去檢測卡頓,而非如何解決卡頓,因此如果對(duì)上面列出的誘因有興趣的讀者可以自行閱讀相關(guān)文章書籍

卡頓檢測

檢測的方案根據(jù)線程是否相關(guān)分為兩大類:

  • 執(zhí)行耗時(shí)任務(wù)會(huì)導(dǎo)致CPU短時(shí)間無法響應(yīng)其他任務(wù),檢測任務(wù)耗時(shí)來判斷是否可能導(dǎo)致卡頓
  • 由于卡頓直接表現(xiàn)為操作無響應(yīng),界面動(dòng)畫遲緩,檢測主線程是否能響應(yīng)任務(wù)來判斷是否卡頓

與主線程相關(guān)的檢測方案包括:

  1. fps
  2. ping
  3. runloop

與主線程不相關(guān)的檢測包括:

  1. stack backtrace
  2. msgSend observe

衡量指標(biāo)

不同方案的檢測原理和實(shí)現(xiàn)機(jī)制都不同,為了更好的選擇所需的方案,需要建立一套衡量指標(biāo)來對(duì)方案進(jìn)行對(duì)比,個(gè)人總結(jié)的衡量指標(biāo)包括四項(xiàng):

  • 卡頓反饋

    卡頓發(fā)生時(shí),檢測方案是否能及時(shí)、直觀的反饋出本次卡頓

  • 采集精度

    卡頓發(fā)生時(shí),檢測方案能否采集到充足的信息來做定位追溯

  • 性能損耗

    維持檢測所需的CPU占用、內(nèi)存使用是否會(huì)引入額外的問題

  • 實(shí)現(xiàn)成本

    檢測方案是否易于實(shí)現(xiàn),代碼的維護(hù)成本與穩(wěn)定性等

fps

通常情況下,屏幕會(huì)保持60hz/s的刷新速度,每次刷新時(shí)會(huì)發(fā)出一個(gè)屏幕刷新信號(hào),CADisplayLink允許我們注冊(cè)一個(gè)與刷新信號(hào)同步的回調(diào)處理??梢酝ㄟ^屏幕刷新機(jī)制來展示fps值:

- (void)startFpsMonitoring {
    WeakProxy *proxy = [WeakProxy proxyWithClient: self];
    self.fpsDisplay = [CADisplayLink displayLinkWithTarget: proxy selector: @selector(displayFps:)];
    [self.fpsDisplay addToRunLoop: [NSRunLoop mainRunLoop] forMode: NSRunLoopCommonModes];
}

- (void)displayFps: (CADisplayLink *)fpsDisplay {
    _count++;
    CFAbsoluteTime threshold = CFAbsoluteTimeGetCurrent() - _lastUpadateTime;
    if (threshold >= 1.0) {
        [FPSDisplayer updateFps: (_count / threshold)];
        _lastUpadateTime = CFAbsoluteTimeGetCurrent();
    }
}
指標(biāo)
卡頓反饋 卡頓發(fā)生時(shí),fps會(huì)有明顯下滑。但轉(zhuǎn)場動(dòng)畫等特殊場景也存在下滑情況。高
采集精度 回調(diào)總是需要cpu空閑才能處理,無法及時(shí)采集調(diào)用棧信息。低
性能損耗 監(jiān)聽屏幕刷新會(huì)頻繁喚醒runloop,閑置狀態(tài)下有一定的損耗。中低
實(shí)現(xiàn)成本 單純的采用CADisplayLink實(shí)現(xiàn)。低
結(jié)論 更適用于開發(fā)階段,線上可作為輔助手段

ping

ping是一種常用的網(wǎng)絡(luò)測試工具,用來測試數(shù)據(jù)包是否能到達(dá)ip地址。在卡頓發(fā)生的時(shí)候,主線程會(huì)出現(xiàn)短時(shí)間內(nèi)無響應(yīng)這一表現(xiàn),基于ping的思路從子線程嘗試通信主線程來獲取主線程的卡頓延時(shí):

@interface PingThread : NSThread
......
@end

@implementation PingThread

- (void)main {
    [self pingMainThread];
}

- (void)pingMainThread {
    while (!self.cancelled) {
        @autoreleasepool {
            dispatch_async(dispatch_get_main_queue(), ^{
                [_lock unlock];
            });
            
            CFAbsoluteTime pingTime = CFAbsoluteTimeGetCurrent();
            NSArray *callSymbols = [StackBacktrace backtraceMainThread];
            [_lock lock];
            if (CFAbsoluteTimeGetCurrent() - pingTime >= _threshold) {
                ......
            }
            [NSThread sleepForTimeInterval: _interval];
        }
    }
}

@end
指標(biāo)
卡頓反饋 主線程出現(xiàn)堵塞直到空閑期間都無法回包,但在ping之間的卡頓存在漏查情況。中高
采集精度 子線程在ping前能獲取主線程準(zhǔn)確的調(diào)用棧信息。中高
性能損耗 需要常駐線程和采集調(diào)用棧。中
實(shí)現(xiàn)成本 需要維護(hù)一個(gè)常駐線程,以及對(duì)象的內(nèi)存控制。中低
結(jié)論 監(jiān)控能力、性能損耗和ping頻率都成正比,監(jiān)控效果強(qiáng)

runloop

作為和主線程相關(guān)的最后一個(gè)方案,基于runloop的檢測和fps的方案非常相似,都需要依賴于主線程的runloop。由于runloop會(huì)調(diào)起同步屏幕刷新的callback,如果loop的間隔大于16.67ms,fps自然達(dá)不到60hz。而在一個(gè)loop當(dāng)中存在多個(gè)階段,可以監(jiān)控每一個(gè)階段停留了多長時(shí)間:

- (void)startRunLoopMonitoring {
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        if (CFAbsoluteTimeGetCurrent() - _lastActivityTime >= _threshold) {
            ......
            _lastActivityTime = CFAbsoluteTimeGetCurrent();
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
指標(biāo)
卡頓反饋 runloop的不同階段把時(shí)間分片,如果某個(gè)時(shí)間片太長,基本認(rèn)定發(fā)生了卡頓。此外應(yīng)用閑置狀態(tài)常駐beforeWaiting階段,此階段存在誤報(bào)可能。中
采集精度 fps類似的,依附于主線程callback的方案缺少準(zhǔn)確采集調(diào)用棧的時(shí)機(jī),但優(yōu)于fps檢測方案。中低
性能損耗 此方案不會(huì)頻繁喚醒runloop,相較于fps性能更佳。低
實(shí)現(xiàn)成本 需要注冊(cè)runloop observer。中低
結(jié)論 綜合性能優(yōu)于fps,但反饋表現(xiàn)不足,只適合作為輔助工具使用

stack backtrace

代碼質(zhì)量不夠好的方法可能會(huì)在一段時(shí)間內(nèi)持續(xù)占用CPU的資源,換句話說在一段時(shí)間內(nèi),調(diào)用??偸峭A粼趫?zhí)行某個(gè)地址指令的狀態(tài)。由于函數(shù)調(diào)用會(huì)發(fā)生入棧行為,如果比對(duì)兩次調(diào)用棧的符號(hào)信息,前者是后者的符號(hào)子集時(shí),可以認(rèn)為出現(xiàn)了卡頓惡鬼

@interface StackBacktrace : NSThread
......
@end

@implementation StackBacktrace

- (void)main {
    [self backtraceStack];
}

- (void)backtraceStack {
    while (!self.cancelled) {
        @autoreleasepool {
            NSSet *curSymbols = [NSSet setWithArray: [StackBacktrace backtraceMainThread]];
            if ([_saveSymbols isSubsetOfSet: curSymbols]) {
                ......
            }
            _saveSymbols = curSymbols;
            [NSThread sleepForTimeInterval: _interval];
        }
    }
}

@end
指標(biāo)
卡頓反饋 由于符號(hào)地址的唯一性,調(diào)用棧比對(duì)的準(zhǔn)確性高。但需要排除閑置狀態(tài)下的調(diào)用棧信息。高
采集精度 直接通過調(diào)用棧符號(hào)信息比對(duì)可以準(zhǔn)確的獲取調(diào)用棧信息。高
性能損耗 需要頻繁獲取調(diào)用棧,需要考慮延后符號(hào)化的時(shí)機(jī)減少損耗。中高
實(shí)現(xiàn)成本 需要維護(hù)常駐線程和調(diào)用棧追溯算法。中高
結(jié)論 準(zhǔn)確率很高的工具,適用面廣

msgSend observe

OC方法的調(diào)用最終轉(zhuǎn)換成msgSend的調(diào)用執(zhí)行,通過在函數(shù)前后插入自定義的函數(shù)調(diào)用,維護(hù)一個(gè)函數(shù)棧結(jié)構(gòu)可以獲取每一個(gè)OC方法的調(diào)用耗時(shí),以此進(jìn)行性能分析與優(yōu)化:

#define save() \
__asm volatile ( \
    "stp x8, x9, [sp, #-16]!\n" \
    "stp x6, x7, [sp, #-16]!\n" \
    "stp x4, x5, [sp, #-16]!\n" \
    "stp x2, x3, [sp, #-16]!\n" \
    "stp x0, x1, [sp, #-16]!\n");

#define resume() \
__asm volatile ( \
    "ldp x0, x1, [sp], #16\n" \
    "ldp x2, x3, [sp], #16\n" \
    "ldp x4, x5, [sp], #16\n" \
    "ldp x6, x7, [sp], #16\n" \
    "ldp x8, x9, [sp], #16\n" );
    
#define call(b, value) \
    __asm volatile ("stp x8, x9, [sp, #-16]!\n"); \
    __asm volatile ("mov x12, %0\n" :: "r"(value)); \
    __asm volatile ("ldp x8, x9, [sp], #16\n"); \
    __asm volatile (#b " x12\n");


__attribute__((__naked__)) static void hook_Objc_msgSend() {

    save()
    __asm volatile ("mov x2, lr\n");
    __asm volatile ("mov x3, x4\n");
    
    call(blr, &push_msgSend)
    resume()
    call(blr, orig_objc_msgSend)
    
    save()
    call(blr, &pop_msgSend)
    
    __asm volatile ("mov lr, x0\n");
    resume()
    __asm volatile ("ret\n");
}
指標(biāo)
卡頓反饋
采集精度
性能損耗 攔截后調(diào)用頻次非常高,啟動(dòng)階段可達(dá)10w次以上調(diào)用。高
實(shí)現(xiàn)成本 需要維護(hù)方法棧和優(yōu)化攔截算法。高
結(jié)論 準(zhǔn)確率很高的工具,但不適用于Swift代碼

總結(jié)

fps ping runloop stack backtrace msgSend observe
卡頓反饋 中高
采集精度 中高 中低
性能損耗 中低 中高
實(shí)現(xià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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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