什么叫卡顿
CPU与GPU两者总共耗时超出视屏刷新帧最小间隔时间T。
如果1s刷新正常按照60帧计算,每帧时间大概为16.7ms。如果出现T多次超出这个范围,导致我们1s实际显示帧数远小于60,我们可以定义为产生了卡顿
CPU和GPU
- 在屏幕成像的过程中,CPU和GPU起着至关重要的作用
- CPU(Central Processing Unit,中央处理器) 对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)
- GPU(Graphics Processing Unit,图形处理器) 纹理的渲染
- 在iOS中是双缓冲机制,有前帧缓存、后帧缓存

屏幕成像原理
GPU 通常有一个机制叫做垂直同步(简写 V-Sync),通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。CRT 的电子枪按照上面方式,从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;
简单来说,就是产生一个VSync,之后不断的进行水平同步信号HSync将屏幕显示完,再产生下一个VSync,再不断的进行水平同步信号HSync将屏幕显示完,重复这样的操作。

按照60FPS的刷帧率,每隔16ms就会有一次VSync信号。1秒是1000ms,1000/60 = 16。
卡顿原因分析

屏幕成像原理:
- CPU计算显示内容,例如视图创建,布局计算、图片解码、文本绘制等;
- 接着 CPU 会将计算好的内容提交到 GPU进行合成、渲染。
- 随后 GPU 会把渲染结果提交到帧缓冲区去,等待VSync 信号到来时显示到屏幕上。
如果此时下一个VSync 信号到来时,CPU或GPU都没有完成相应的工作时,则那一帧将会丢失
- 如图第3步:VSync信号回来时,GPU还没有完成相应的工作,这一帧将会丢失
- 如图第4步:当第3步丢失了,可能会导致第4步操作缺失,这一步也会丢帧
卡顿造成的原因通常是CPU和GPU导致的掉帧引起的,主要原因如下:
- 主线程在进行大量I/O操作:为了方便代码编写,直接在主线程去写入大量数据;
- 主线程在进行大量计算:代码编写不合理,主线程进行复杂计算;
- 大量UI绘制:界面过于复杂,UI绘制需要大量时间;
- 主线程在等锁:主线程需要获得锁A,但是当前某个子线程持有这个锁A,导致主线程不得不等待主线程完成任务。
怎么监控应用的卡顿情况
- FPS 监控:这是最容易想到的一种方案,如果帧率越高意味着界面越流畅,上文也给出了计算 FPS 的实现方式,通过一段连续的 FPS 计算丢帧率来衡量当前页面绘制的质量
- 主线程卡顿监控:这是业内常用的一种检测卡顿的方法,通过开辟一个子线程来监控主线程的 RunLoop,当两个状态区域之间的耗时大于阈值时,就记为发生一次卡顿。美团的移动端性能监控方案 Hertz 采用的就是这种方式
Runloop监控卡顿
原理: 平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作,可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的。 
runloop的伪代码
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
/// 5. GCD处理main block
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}发现NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿。
那么,我们卡顿监控在 Runloop 的起始最开始和结束最末尾位置添加 Observer,从而获得主线程的开始和结束状态。卡顿监控起一个子线程定时检查主线程的状态,当主线程的状态运行超过一定阈值则认为主线程卡顿,从而标记为一个卡顿
使用Runloop进行卡顿监控之后,需要定义一个阀值来判定卡顿的出现
- 主程序 Runloop 超时的阈值是 2 秒,子线程的检查周期是 1 秒。每隔 1 秒,子线程检查主线程的运行状态;如果检查到主线程 Runloop 运行超过 2 秒则认为是卡顿,并获得当前的线程快照, 获取当前的调用栈。
- 假定连续5次超时50ms认为卡顿(当然也包含了单次超时250ms)
// 开始监听
- (void)startMonitor {
if (observer) {
return;
}
// 创建信号
semaphore = dispatch_semaphore_create(0);
NSLog(@"dispatch_semaphore_create:%@",[BGPerformanceMonitor getCurTime]);
// 注册RunLoop状态观察
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//创建Run loop observer对象
//第一个参数用于分配observer对象的内存
//第二个参数用以设置observer所要关注的事件,详见回调函数myRunLoopObserver中注释
//第三个参数用于标识该observer是在第一次进入run loop时执行还是每次进入run loop处理时均执行
//第四个参数用于设置该observer的优先级
//第五个参数用于设置该observer的回调函数
//第六个参数用于设置该observer的运行环境
observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES) { // 有信号的话 就查询当前runloop的状态
// 假定连续5次超时50ms认为卡顿(当然也包含了单次超时250ms)
// 因为下面 runloop 状态改变回调方法runLoopObserverCallBack中会将信号量递增 1,所以每次 runloop 状态改变后,下面的语句都会执行一次
// dispatch_semaphore_wait:Returns zero on success, or non-zero if the timeout occurred.
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
NSLog(@"dispatch_semaphore_wait:st=%ld,time:%@",st,[self getCurTime]);
if (st != 0) { // 信号量超时了 - 即 runloop 的状态长时间没有发生变更,长期处于某一个状态下
if (!observer) {
timeoutCount = 0;
semaphore = 0;
activity = 0;
return;
}
NSLog(@"st = %ld,activity = %lu,timeoutCount = %d,time:%@",st,activity,timeoutCount,[self getCurTime]);
// kCFRunLoopBeforeSources - 即将处理source kCFRunLoopAfterWaiting - 刚从休眠中唤醒
// 获取kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting的状态就可以知道是否有卡顿的情况。
// kCFRunLoopBeforeSources:停留在这个状态,表示在做很多事情
if (activity == kCFRunLoopBeforeSources || activity == kCFRunLoopAfterWaiting) { // 发生卡顿,记录卡顿次数
if (++timeoutCount < 5) {
continue; // 不足 5 次,直接 continue 当次循环,不将timeoutCount置为0
}
// 收集Crash信息也可用于实时获取各线程的调用堆栈
PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
NSData *data = [crashReporter generateLiveReport];
PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter withTextFormat:PLCrashReportTextFormatiOS];
NSLog(@"---------卡顿信息\n%@\n--------------",report);
}
}
NSLog(@"dispatch_semaphore_wait timeoutCount = 0,time:%@",[self getCurTime]);
timeoutCount = 0;
}
});
}