監測APP卡頓
一、UI更新原理和卡頓原因
在 VSync 訊號到來後,系統圖形服務會通過 CADisplayLink 等機制通知 App,App 主執行緒開始在 CPU 中計算顯示內容,比如檢視的建立、佈局計算、圖片解碼、文字繪製等。隨後 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。隨後 GPU 會把渲染結果提交到幀緩衝區去,等待下一次 VSync 訊號到來時顯示到螢幕上。由於垂直同步的機制,如果在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是介面卡頓的原因。
所以,卡頓造成的原因分為CPU卡頓和GPU卡頓,CPU卡頓可以用CADisplayLink來檢測,UI更新卡頓可以用Runloop的mode來檢測
-
監測卡頓:開一個子執行緒,利用displaylink或者Runloop來監測卡頓;
-
收集堆疊:將卡頓時的堆疊收集起來;
-
上傳記錄:將卡頓上傳到後臺或自定義;
這裡我引用一張微信開發團隊的監測流程圖:
二、Runloop檢測卡頓
首先我們來看一個Runloop的執行方式,如下
Objectiveint32_t __CFRunLoopRun() { // 通知即將進入runloop //建立AutoreleasePool: _objc_autoreleasePoolPush(); __CFRunLoopDoObservers(KCFRunLoopEntry); do { // 通知將要處理timer和source __CFRunLoopDoObservers(kCFRunLoopBeforeTimers); __CFRunLoopDoObservers(kCFRunLoopBeforeSources); // 處理非延遲的主執行緒呼叫 __CFRunLoopDoBlocks(); // 處理UIEvent事件 __CFRunLoopDoSource0(); // GCD dispatch main queue CheckIfExistMessagesInMainDispatchQueue(); // 即將進入休眠 //釋放並新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush(); __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting); // 等待核心mach_msg事件 mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts(); // Zzz... // 從等待中醒來 __CFRunLoopDoObservers(kCFRunLoopAfterWaiting); if (wakeUpPort == timerPort){// 處理因timer的喚醒 __CFRunLoopDoTimers(); }else if (wakeUpPort == mainDispatchQueuePort){// 處理非同步方法喚醒,如dispatch_async __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() } else{// UI重新整理,動畫顯示 __CFRunLoopDoSource1(); } // 再次確保是否有同步的方法需要呼叫 __CFRunLoopDoBlocks(); } while (!stop && !timeout); // 通知即將退出runloop //釋放AutoreleasePool: _objc_autoreleasePoolPop(); __CFRunLoopDoObservers(CFRunLoopExit); }
UI更新一般kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間,所以我們監測他們之間的時間段就能知道UI是否卡頓了
Plaintext- (void)startMoniter{ //新增監聽 CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL}; _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context); CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes); // 建立訊號 _semaphore = dispatch_semaphore_create(0); // 在子執行緒監控時長 dispatch_async(dispatch_get_global_queue(0, 0), ^{ while (YES) { NSLog(@"smooth--monitering"); //100ms則將堆疊記錄下來 long st = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, 100*NSEC_PER_MSEC)); if (st != 0) { if (_activity==kCFRunLoopBeforeSources || _activity==kCFRunLoopAfterWaiting) { [self logStack]; } } } }); } static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { SmoothMoniter *instrance = [SmoothMoniter sharedInstance]; instrance.activity = activity; dispatch_semaphore_t semaphore = instrance.semaphore; dispatch_semaphore_signal(semaphore); }
三、收集堆疊
收集堆疊資訊以用來分析卡頓引起的程式碼
#import <libkern/OSAtomic.h>
#import <execinfo.h>
Plaintext
- (void)logStack{
void* callstack[128];
int frames = backtrace(callstack, 128);
char **strs = backtrace_symbols(callstack, frames);
int i;
NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
for ( i = 0 ; i < frames ; i++ ){
[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
}
Plaintext
可以得到類似於下方的堆疊記錄
四、DisplayLink檢測卡頓
一但 CADisplayLink 以特定的模式註冊到runloop之後,每當螢幕需要重新整理的時候,runloop就會呼叫CADisplayLink繫結的target上的selector,這時target可以讀到 CADisplayLink 的每次呼叫的時間戳,用來準備下一幀顯示需要的資料。所以通過比較dispalylink的更新時間就可以知道是否存在卡頓
- (void)updateTime{
if (!_last_time) {
_last_time = self.displayLink.timestamp;
return;
}
_count ++;
CFTimeInterval current = self.displayLink.timestamp;
CFTimeInterval period = current - _last_time;
if (period > 1 ) {
NSLog(@"FPS:%@",@(_count));
_count = 0;
_last_time = self.displayLink.timestamp;
}
}
- (CADisplayLink *)displayLink{
if (!_displayLink) {
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateTime)];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
return _displayLink;
}
Plaintext
五、上傳記錄
1、頻率以及流量:是否所有的使用者都要做統計?上傳的頻率?檔案壓縮以減少流量?這些問題都要根據實際情況作好準備。
2、上傳位置,一種是自己建立後臺來統計這些卡頓,嫌麻煩的話是利用第三方平臺、如友盟(統計崩潰比較多)、聽雲、OneApm、博睿,都大同小異。
六、程式碼
上面的程式碼可以在smoothMonitor下載
http://www.helloted.com/ios/2016/10/06/smoothMonitor/
------------------越是喧囂的世界,越需要寧靜的思考------------------ 合抱之木,生於毫末;九層之臺,起於壘土;千里之行,始於足下。 積土成山,風雨興焉;積水成淵,蛟龍生焉;積善成德,而神明自得,聖心備焉。故不積跬步,無以至千里;不積小流,無以成江海。騏驥一躍,不能十步;駑馬十駕,功在不捨。鍥而舍之,朽木不折;鍥而不捨,金石可鏤。蚓無爪牙之利,筋骨之強,上食埃土,下飲黃泉,用心一也。蟹六跪而二螯,非蛇鱔之穴無可寄託者,用心躁也。