質量監控-卡頓檢測

原文連接html

不論是應用秒變幻燈片,仍是啓動太久被殺,基本都是開發者必經的體驗。就像沒人但願堵車同樣,卡頓永遠是不受用戶歡迎的,因此如何發現卡頓是開發者須要直面的難題。雖然致使卡頓的緣由有不少,但卡頓的表現老是大同小異。若是把卡頓當作病症看待,二者分別對應所謂的本與標。要檢測卡頓,不管是標或本均可如下手,但都須要深刻的學習算法

instruments與性能

在開發階段,使用內置的性能工具instruments來檢測性能問題是最佳的選擇。與應用運行性能關聯最緊密的兩個硬件CPUGPU,前者用於執行程序指令,針對代碼的處理邏輯;後者用於大量計算,針對圖像信息的渲染。正常狀況下,CPU會週期性的提交要渲染的圖像信息給GPU處理,保證視圖的更新。一旦其中之一響應不過來,就會表現爲卡頓。所以多數狀況下用到的工具是檢測GPU負載的Core Animation,以及檢測CPU處理效率的Time Profilermarkdown

因爲CPU提交圖像信息是在主線程執行的,會影響到CPU性能的誘因包括如下:網絡

  1. 發生在主線程的I/O任務
  2. 過多的線程搶佔CPU資源
  3. 溫度太高致使的CPU降頻

而影響GPU的因素較爲客觀,難以針對作代碼上的優化,包括:async

  1. 顯存頻率
  2. 渲染算法
  3. 大計算量

本文旨在介紹如何去檢測卡頓,而非如何解決卡頓,所以若是對上面列出的誘因有興趣的讀者能夠自行閱讀相關文章書籍函數

卡頓檢測

檢測的方案根據線程是否相關分爲兩大類:工具

  • 執行耗時任務會致使CPU短期沒法響應其餘任務,檢測任務耗時來判斷是否可能致使卡頓
  • 因爲卡頓直接表現爲操做無響應,界面動畫遲緩,檢測主線程是否能響應任務來判斷是否卡頓

與主線程相關的檢測方案包括:oop

  1. fps
  2. ping
  3. runloop

與主線程不相關的檢測包括:性能

  1. stack backtrace
  2. msgSend observe

衡量指標

不一樣方案的檢測原理和實現機制都不一樣,爲了更好的選擇所需的方案,須要創建一套衡量指標來對方案進行對比,我的總結的衡量指標包括四項:學習

  • 卡頓反饋

    卡頓發生時,檢測方案是否能及時、直觀的反饋出本次卡頓

  • 採集精度

    卡頓發生時,檢測方案可否採集到充足的信息來作定位追溯

  • 性能損耗

    維持檢測所需的CPU佔用、內存使用是否會引入額外的問題

  • 實現成本

    檢測方案是否易於實現,代碼的維護成本與穩定性等

fps

一般狀況下,屏幕會保持60hz/s的刷新速度,每次刷新時會發出一個屏幕刷新信號,CADisplayLink容許咱們註冊一個與刷新信號同步的回調處理。能夠經過屏幕刷新機制來展現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();
    }
}
複製代碼
指標
卡頓反饋 卡頓發生時,fps會有明顯下滑。但轉場動畫等特殊場景也存在下滑狀況。高
採集精度 回調老是須要cpu空閒才能處理,沒法及時採集調用棧信息。低
性能損耗 監聽屏幕刷新會頻繁喚醒runloop,閒置狀態下有必定的損耗。中低
實現成本 單純的採用CADisplayLink實現。低
結論 更適用於開發階段,線上可做爲輔助手段

ping

ping是一種經常使用的網絡測試工具,用來測試數據包是否能到達ip地址。在卡頓發生的時候,主線程會出現短期內無響應這一表現,基於ping的思路從子線程嘗試通訊主線程來獲取主線程的卡頓延時:

@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
複製代碼
指標
卡頓反饋 主線程出現堵塞直到空閒期間都沒法回包,但在ping之間的卡頓存在漏查狀況。中高
採集精度 子線程在ping前能獲取主線程準確的調用棧信息。中高
性能損耗 須要常駐線程和採集調用棧。中
實現成本 須要維護一個常駐線程,以及對象的內存控制。中低
結論 監控能力、性能損耗和ping頻率都成正比,監控效果強

runloop

做爲和主線程相關的最後一個方案,基於runloop的檢測和fps的方案很是類似,都須要依賴於主線程的runloop。因爲runloop會調起同步屏幕刷新的callback,若是loop的間隔大於16.67msfps天然達不到60hz。而在一個loop當中存在多個階段,能夠監控每個階段停留了多長時間:

- (void)startRunLoopMonitoring {
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        if (CFAbsoluteTimeGetCurrent() - _lastActivityTime >= _threshold) {
            ......
            _lastActivityTime = CFAbsoluteTimeGetCurrent();
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
複製代碼
指標
卡頓反饋 runloop的不一樣階段把時間分片,若是某個時間片太長,基本認定發生了卡頓。此外應用閒置狀態常駐beforeWaiting階段,此階段存在誤報可能。中
採集精度 fps相似的,依附於主線程callback的方案缺乏準確採集調用棧的時機,但優於fps檢測方案。中低
性能損耗 此方案不會頻繁喚醒runloop,相較於fps性能更佳。低
實現成本 須要註冊runloop observer。中低
結論 綜合性能優於fps,但反饋表現不足,只適合做爲輔助工具使用

stack backtrace

代碼質量不夠好的方法可能會在一段時間內持續佔用CPU的資源,換句話說在一段時間內,調用棧老是停留在執行某個地址指令的狀態。因爲函數調用會發生入棧行爲,若是比對兩次調用棧的符號信息,前者是後者的符號子集時,能夠認爲出現了卡頓惡鬼

@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
複製代碼
指標
卡頓反饋 因爲符號地址的惟一性,調用棧比對的準確性高。但須要排除閒置狀態下的調用棧信息。高
採集精度 直接經過調用棧符號信息比對能夠準確的獲取調用棧信息。高
性能損耗 須要頻繁獲取調用棧,須要考慮延後符號化的時機減小損耗。中高
實現成本 須要維護常駐線程和調用棧追溯算法。中高
結論 準確率很高的工具,適用面廣

msgSend observe

OC方法的調用最終轉換成msgSend的調用執行,經過在函數先後插入自定義的函數調用,維護一個函數棧結構能夠獲取每個OC方法的調用耗時,以此進行性能分析與優化:

#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");
}
複製代碼
指標
卡頓反饋
採集精度
性能損耗 攔截後調用頻次很是高,啓動階段可達10w次以上調用。高
實現成本 須要維護方法棧和優化攔截算法。高
結論 準確率很高的工具,但不適用於Swift代碼

總結

fps ping runloop stack backtrace msgSend observe
卡頓反饋 中高
採集精度 中高 中低
性能損耗 中低 中高
實現成本 中低 中低 中高

關注個人公衆號獲取更新信息
相關文章
相關標籤/搜索