原文連接html
不論是應用秒變幻燈片,仍是啓動太久被殺,基本都是開發者必經的體驗。就像沒人但願堵車同樣,卡頓永遠是不受用戶歡迎的,因此如何發現卡頓是開發者須要直面的難題。雖然致使卡頓的緣由有不少,但卡頓的表現老是大同小異。若是把卡頓當作病症看待,二者分別對應所謂的本與標。要檢測卡頓,不管是標或本均可如下手,但都須要深刻的學習算法
在開發階段,使用內置的性能工具instruments
來檢測性能問題是最佳的選擇。與應用運行性能關聯最緊密的兩個硬件CPU
和GPU
,前者用於執行程序指令,針對代碼的處理邏輯;後者用於大量計算,針對圖像信息的渲染。正常狀況下,CPU
會週期性的提交要渲染的圖像信息給GPU
處理,保證視圖的更新。一旦其中之一響應不過來,就會表現爲卡頓。所以多數狀況下用到的工具是檢測GPU
負載的Core Animation
,以及檢測CPU
處理效率的Time Profiler
markdown
因爲CPU
提交圖像信息是在主線程執行的,會影響到CPU
性能的誘因包括如下:網絡
I/O
任務CPU
資源CPU
降頻而影響GPU
的因素較爲客觀,難以針對作代碼上的優化,包括:async
本文旨在介紹如何去檢測卡頓,而非如何解決卡頓,所以若是對上面列出的誘因有興趣的讀者能夠自行閱讀相關文章書籍函數
檢測的方案根據線程是否相關分爲兩大類:工具
CPU
短期沒法響應其餘任務,檢測任務耗時來判斷是否可能致使卡頓與主線程相關的檢測方案包括:oop
fps
ping
runloop
與主線程不相關的檢測包括:性能
stack backtrace
msgSend observe
不一樣方案的檢測原理和實現機制都不一樣,爲了更好的選擇所需的方案,須要創建一套衡量指標來對方案進行對比,我的總結的衡量指標包括四項:學習
卡頓反饋
卡頓發生時,檢測方案是否能及時、直觀的反饋出本次卡頓
採集精度
卡頓發生時,檢測方案可否採集到充足的信息來作定位追溯
性能損耗
維持檢測所需的CPU
佔用、內存使用是否會引入額外的問題
實現成本
檢測方案是否易於實現,代碼的維護成本與穩定性等
一般狀況下,屏幕會保持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
是一種經常使用的網絡測試工具,用來測試數據包是否能到達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
的檢測和fps
的方案很是類似,都須要依賴於主線程的runloop
。因爲runloop
會調起同步屏幕刷新的callback
,若是loop
的間隔大於16.67ms
,fps
天然達不到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 ,但反饋表現不足,只適合做爲輔助工具使用 |
代碼質量不夠好的方法可能會在一段時間內持續佔用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
複製代碼
指標 | |
---|---|
卡頓反饋 | 因爲符號地址的惟一性,調用棧比對的準確性高。但須要排除閒置狀態下的調用棧信息。高 |
採集精度 | 直接經過調用棧符號信息比對能夠準確的獲取調用棧信息。高 |
性能損耗 | 須要頻繁獲取調用棧,須要考慮延後符號化的時機減小損耗。中高 |
實現成本 | 須要維護常駐線程和調用棧追溯算法。中高 |
結論 | 準確率很高的工具,適用面廣 |
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 | |
---|---|---|---|---|---|
卡頓反饋 | 高 | 中高 | 中 | 高 | 高 |
採集精度 | 低 | 中高 | 中低 | 高 | 高 |
性能損耗 | 中低 | 中 | 低 | 中高 | 高 |
實現成本 | 低 | 中低 | 中低 | 中高 | 高 |