本文主要是从FPS
、主线程卡顿
和子线程操作UI
三个方面监测卡顿
一、FPS
1.监控原理
主要是基于CADisplayLink
以屏幕刷新频率同步绘图的特性,尝试根据这点去实现一个可以观察屏幕当前帧数的指示器。
基于CADisplayLink实现的 FPS 在生产场景中只有指导意义,不能代表真实的 FPS,因为基于CADisplayLink实现的 FPS 无法完全检测出当前 Core Animation 的性能情况,它只能检测出当前 RunLoop 的帧率。
FPS 的刷新频率非常快,并且容易发生抖动,因此直接通过比较 FPS 来侦测卡顿是比较困难的。
2.实现方式
- CADisplayLink 默认每秒 60次;
- 将 CADisplayLink add 到 mainRunLoop 中;
- 使用 CADisplayLink 的 timestamp 属性,在 CADisplayLink 每次 tick 时,记录上一次的 timestamp;
- 用 count 记录 CADisplayLink tick 的执行次数;
- 计算此次 tick 时, CADisplayLink 的当前 timestamp 和 lastTimeStamp 的差值;
- 如果差值大于1,fps = count / delta,计算得出 FPS 数。
代码如下:
- (void)startMonitorWithBlock:(MCXFPSMonitorBlock)monitorBlock {
self.isMonitoring = YES;
self.monitorBlock = monitorBlock;
if (self.displayLink) {
self.displayLink.paused = NO;
return;
}
__weak typeof(self) weakSelf = self;
self.displayLink = [CADisplayLink mcx_displayLinkWithBlock:^(CADisplayLink * _Nonnull displayLink) {
[weakSelf tick:displayLink];
}];
//self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)tick:(CADisplayLink *)link {
if (self.lastTime == 0) {
self.lastTime = link.timestamp;
return;
}
self.count++;
NSTimeInterval delta = link.timestamp - self.lastTime;
if (delta < 1) {
return;
}
self.lastTime = link.timestamp;
double fps = self.count / delta;
if (self.monitorBlock) {
self.monitorBlock(self, fps);
}
self.count = 0;
}
3.使用CADisplayLink的坑
通常代码如下
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
CADisplayLink/NSTimer出于设计考虑会强引用target,同时NSRunLoop又会持有持有CADisplayLink/NSTimer,使用方同时又持有了CADisplayLink/NSTimer,这就造成了循环引用,示意图如下:
解决方案一、Block形式
封装一个CADisplayLink的Category,提供block形式的接口: /* 在初始化NSTimer/CADisplayLink对象时候,指定target时候,会保留其目标对象。我们的目的是绕开这个定时器对象强引用目标对象这个问题。在分类中,定时器对象指定的target是NSTimer/CADisplayLink类对象,这是个单例,因此计时器是否会保留它都无所谓。这么做,循环引用依然存在,但是因为类对象无需回收,所以能解决问题。
+ (CADisplayLink *)mcx_displayLinkWithBlock:(MCXDisplayLinkBlock)block {
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(mcx_displayLink:)];
displayLink.mcx_displayLinkBlock = [block copy];
return displayLink;
}
+ (void)mcx_displayLink:(CADisplayLink *)displayLink {
if (displayLink.mcx_displayLinkBlock) {
displayLink.mcx_displayLinkBlock(displayLink);
}
}
- (void)setMcx_displayLinkBlock:(MCXDisplayLinkBlock)mcx_displayLinkBlock {
objc_setAssociatedObject(self, @selector(mcx_displayLinkBlock), mcx_displayLinkBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (MCXDisplayLinkBlock)mcx_displayLinkBlock {
return objc_getAssociatedObject(self, @selector(mcx_displayLinkBlock));
}
解决方案二、WeakProxy形式
NSProxy大家在日常开发中使用较少,其实就是一个代理中间人,可以代理其他的类/对象,用在这里很合适。先贴一张示意图:
部分代码如下:
[CADisplayLink displayLinkWithTarget:[MCXWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
MCXWeakProxy.h
...
@property (nullable, nonatomic, weak, readonly) id target;
...
MCXWeakProxy.m
...
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[MCXWeakProxy alloc] initWithTarget:target];
}
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
...
二、主线程卡顿监控
1.监控原理
这是业内常用的一种检测卡顿的方法,通过开辟一个子线程来监控主线程的 RunLoop,当两个状态区域之间的耗时大于阈值时,就记为发生一次卡顿。美团的移动端性能监控方案 Hertz
采用的就是这种方式,如下图
主线程卡顿监控的实现思路:开辟一个子线程,然后实时计算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 两个状态区域之间的耗时是否超过某个阀值,来断定主线程的卡顿情况,可以将这个过程想象成操场上跑圈的运动员,我们会每隔一段时间间隔去判断是否跑了一圈,如果发现在指定时间间隔没有跑完一圈,则认为在消息处理的过程中耗时太多,视为主线程卡顿。
2.图解原理
-
正常情况下
-
卡顿情况下
3.实现方式
- 创建一个观察者runLoopObserver,用于观察主线程的runloop状态。
- 还要创建一个信号量dispatchSemaphore,用于保证同步操作。
- 将观察者runLoopObserver添加到主线程runloop中观察。
- 开启一个子线程,并且在子线程中开启一个持续的loop来监控主线程runloop的状态。
- 如果发现主线程runloop的状态卡在为BeforeSources或者AfterWaiting超过一定时间时,即表明主线程当前卡顿。
具体代码如下
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
MCXCatonMonitor *monitor = (__bridge MCXCatonMonitor*)info;
monitor->runLoopActivity = activity;
dispatch_semaphore_t semaphore = monitor->dispatchSemaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)startMonitor {
self.isMonitoring = YES;
//监测卡顿
if (runLoopObserver) {
return;
}
dispatchSemaphore = dispatch_semaphore_create(0); //Dispatch Semaphore保证同步
//创建一个观察者
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
//将观察者添加到主线程runloop的common模式下的观察中
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
//创建子线程监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//子线程开启一个持续的loop用来进行监控
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, kMCXCatonMonitorDuration * NSEC_PER_MSEC));
if (semaphoreWait != 0) {
if (!self->runLoopObserver) {
self->timeoutCount = 0;
self->dispatchSemaphore = 0;
self->runLoopActivity = 0;
return;
}
//两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
if (self->runLoopActivity == kCFRunLoopBeforeSources || self->runLoopActivity == kCFRunLoopAfterWaiting) {
//出现三次出结果
if (++self->timeoutCount < kMCXCatonMonitorMaxCount) {
continue;
}
//进行堆栈记录保存
} //end activity
}// end semaphore wait
self->timeoutCount = 0;
}// end while
});
}
runloop观察者:Runloop Observer有7种状态
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),//运行runloop的入口。
kCFRunLoopBeforeTimers = (1UL << 1),//(在处理任何Timer计时器之前)
kCFRunLoopBeforeSources = (1UL << 2),//(在处理任何Sources源之前)
kCFRunLoopBeforeWaiting = (1UL << 5),//(在等待源Source和计时器Timer之前)
kCFRunLoopAfterWaiting = (1UL << 6),//(在等待源Source和计时器Timer后,同时在被唤醒之前。)
kCFRunLoopExit = (1UL << 7),//(runloop的出口)
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
三、子线程操作UI
子线程操作也会导致界面卡顿,任何操作UI过程中,正常会触发UIView中setNeedsLayout
、setNeedsDisplay
、setNeedsDisplayInRect
和layoutSubviews
四个方法中至少一个,只需要利用runtime交换这四个方法,然后进行线程检查,然后记录当前线程调用堆栈即可。
+ (void)load {
[self mcx_swizzleSEL:@selector(setNeedsLayout) withSEL:@selector(mcx_setNeedsLayout)];
[self mcx_swizzleSEL:@selector(setNeedsDisplay) withSEL:@selector(mcx_setNeedsDisplay)];
[self mcx_swizzleSEL:@selector(setNeedsDisplayInRect:) withSEL:@selector(mcx_setNeedsDisplayInRect:)];
[self mcx_swizzleSEL:@selector(layoutSubviews) withSEL:@selector(mcx_layoutSubviews)];
}
- (void)mcx_setNeedsLayout {
[self mcx_setNeedsLayout];
[self childThreadUICheck];
}
- (void)mcx_setNeedsDisplay {
[self mcx_setNeedsDisplay];
[self childThreadUICheck];
}
- (void)mcx_setNeedsDisplayInRect:(CGRect)rect {
[self mcx_setNeedsDisplayInRect:rect];
[self childThreadUICheck];
}
- (void)mcx_layoutSubviews {
[self mcx_layoutSubviews];
[self childThreadUICheck];
}
- (void)childThreadUICheck {
if ([NSThread isMainThread]) {
return;
}
//此处有子线程操作UI情况,记录当前线程堆栈信息
}