一、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的運行方式,如下
int32_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是否卡頓了
- (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>
- (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);
}
可以得到類似於下方的堆棧記錄
四、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;
}
五、上傳記錄
1、頻率以及流量:是否所有的用戶都要做統計?上傳的頻率?文件壓縮以減少流量?這些問題都要根據實際情況作好准備。
2、上傳位置,一種是自己建立后台來統計這些卡頓,嫌麻煩的話是利用第三方平台、如友盟(統計崩潰比較多)、聽雲、OneApm、博睿,都大同小異。
六、代碼
上面的代碼可以在smoothMonitor 下載
http://www.helloted.com/ios/2016/10/06/smoothMonitor/