监控APP卡顿的三种方式
- FPS
- 子线程ping主线程
- Runloop
FPS检测APP卡顿
通常情况下,屏幕会保持60hz/s的刷新速度,每次刷新时会发出一个屏幕刷新信号,CADisplayLink
允许我们注册一个与刷新信号同步的回调处理。可以通过屏幕刷新机制来展示fps值:
显示器中是固定的频率,比如iOS中是每秒60帧(60FPS),即每帧16.7ms。从上图中可以看出,每两个VSync信号之间有时间间隔(16.7ms),在这个时间内,CPU主线程计算布局,解码图片,创建视图,绘制文本,计算完成后将内容交给GPU,GPU变换,合成,渲染,放入帧缓冲区。假如16.7ms内,CPU和GPU没有来得及生产出一帧缓冲,那么这一帧会被丢弃,显示器就会保持不变,继续显示上一帧内容,这就将导致导致画面卡顿。所以无论CPU, GPU,哪个消耗时间过长,都会导致在16.7ms内无法生成一帧缓存
主线程为了达到接近60fps的绘制效率,不能在UI线程有单个超过(1/60s≈16ms)的计算任务,否则就会卡顿
- 死锁:主线程拿到锁 A,需要获得锁 B,而同时某个子线程拿了锁 B,需要锁 A,这样相互等待就死锁了。
- 抢锁:主线程需要访问 DB,而此时某个子线程往 DB 插入大量数据。通常抢锁的体验是偶尔卡一阵子,过会就恢复了。
- 主线程大量 IO:主线程为了方便直接写入大量数据,会导致界面卡顿。
- 主线程大量计算:算法不合理,导致主线程某个函数占用大量 CPU。
- 大量的 UI 绘制:复杂的 UI、图文混排等,带来大量的 UI 绘制。
我们可以用CADisplayLink
来检测FPS,CADisplayLink
则是使用帧率来作为时间间隔的单位
- (void)setupDisplayLink {
//创建CADisplayLink,并添加到当前run loop的NSRunLoopCommonModes
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTicks:)];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)linkTicks:(CADisplayLink *)link {
//执行次数
_scheduleTimes ++;
//当前时间戳
if(_timestamp == 0){
_timestamp = link.timestamp;
}
CFTimeInterval timePassed = link.timestamp - _timestamp;
if(timePassed >= 1.f)
//fps
CGFloat fps = _scheduleTimes/timePassed;
printf("fps:%.1f, timePassed:%f\n", fps, timePassed);
//reset
_timestamp = link.timestamp;
_scheduleTimes = 0;
}
}
复制代码
我们也可以通过Xcode中的Instruments中的Core Animation来监测FPS
在上述FPS指示器中,如果将CADisplayLink放置于子线程的Runloop中,将会发生什么?
答案是无论主线程有多么繁忙,GPU占用有多么高,FPS始终是60,原因是基于CADisplayLink的FPS指示器只能检测到当前RunLoop的FPS
Runloop如何检测卡顿
由于runloop会调起同步屏幕刷新的callback,如果loop的间隔大于16.67ms,fps自然达不到60hz。而在一个loop当中存在多个阶段,可以监控每一个阶段停留了多长时间
- (void)startRunloopMonitorFreeze {
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
///如果CFAbsoluteTimeGetCurrent() - 上一次的时间差 比阈值大,说明卡顿了,APP处于闲置状态常驻beforeWaiting状态,判断不准确
});
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
复制代码
这种检测方式当APP处于闲置状态Runloop常驻beforeWaiting状态,判断不准确