質量監控-卡頓檢測

原文鏈接

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

instruments與性能

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

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

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

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

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

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

卡頓檢測

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

  • 執行耗時任務會導致CPU短時間無法響應其他任務,檢測任務耗時來判斷是否可能導致卡頓
  • 由于卡頓直接表現為操作無響應,界面動畫遲緩,檢測主線程是否能響應任務來判斷是否卡頓

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

  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
卡頓反饋 中高
采集精度 中高 中低
性能損耗 中低 中高
實現成本 中低 中低 中高
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380

推薦閱讀更多精彩內容