卡顿
通过观察者注册到RunLoop可以在以下几个阶段收到通知
kCFRunLoopEntry
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
kCFRunLoopAfterWaiting
kCFRunLoopExit
kCFRunLoopAllActivities
__CFRunLoopRun具体源码分析可见这篇博客,根据源码提炼出Runloop循环的主要流程如下图:
RunLoop处理的事件源归类
RunLoop处理的事件源总体其实可以分为两大类,第一类的source0,第二类类是基于MachPort的消息。
平时UIView的事件处理以及代码中定义的block调用都属于source0,而线程之间的消息通讯,通过GCD派
发到主线程的任务以及NSTimer都是基于MachPort消息实现的。所以在流程图中,我将第二类进行了
突出,表示它们是一大类型。在官方文档中,它分为timer,source,Oberserver,source又分
为source0和source1,单独将timer拿出来。
RunLoop的哪些状态值得被关注
source0的任务执行前 RunLoop处于的状态是KCFRunLoopBeforeSources
基于machPort的任务执行前 RunLoop处于的状态是kCFRunLoopBeforeSources或者
kCFRunLoopAfterWaiting所以只需要监听Runloop的状态在CFRunLoopBeforeSources和
CFRunLoopAfterWaiting这两个状态是否产生了制定时间的执行时间就可以判断是否发生卡顿了。
而CFRunLoopBeforeWaiting之后,Runloop就进入睡眠了,没有处理其他事件源的逻辑。
如果监控该状态,在主线程睡眠的时候,就会判断为卡顿了,产生误报。
监控主线程Runloop的状态变化
开启一个子线程,让子线程去检测主线程Runloop的状态变化。在每次Runloop状态发生变化的时候,通过
dispatch_semaphore_signal发送一个信号,使得信号量的值加1.让子线程一直关注semaphore的变化,
如果主线程在Runloop某一个状态停留太久,那么semaphore会让子线程阻塞,直到超时,即发生了卡顿。
//全局或者成员变量
CFRunLoopActivity currentActivity;
//信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
//检测主线程Runloop状态
CFRunLoopObserverRef observerRef = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES, 0,
^(CFRunLoopObserverRef observer,
CFRunLoopActivity activity) {
if (self != nil && semaphore != NULL) {
currentActivity = activity;
dispatch_semaphore_signal(semaphore);
}
});
CFRunLoopAddObserver(CFRunLoopGetMain(), observerRef, kCFRunLoopCommonModes);
CFRelease(observerRef);
//通过信号量计算某一状态下是否执行超过制定时间,这里是1s
while (!self.cancelled) {
long status = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW,
1000 * NSEC_PER_MSEC));
if (status != 0) {
if (self.cancelled || semaphore == NULL) {
return;
}
if (currentActivity == kCFRunLoopBeforeSources ||
currentActivity == kCFRunLoopAfterWaiting ) {
if (self.cancelled) return;
//发生了卡顿
NSLog(@"卡顿了");
if (self.cancelled) return;
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
}
}
最后用一张图来展示整个流程:
FPS
CADisplayLink是与屏幕刷新保持同步的定时器,通过它可以计算出实时的屏幕帧率。
在理想情况下,每秒60帧,CADdisplayLink回调的时间间隔就是1/60 ≈ 0.0167秒。
在runloop每次迭代的过程中,如果有耗时的任务,会增加CADdisplayLink回调时长,相对来说,计算出来
的帧率就会较小。屏幕绘制的时候,发生跳帧。
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_tick:)];
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
- (void)p_tick:(CADisplayLink *)link
{
self.count++;
if (!self.lastTime) {
self.lastTime = CACurrentMediaTime();
} else {
NSTimeInterval currentTime = CACurrentMediaTime();
NSTimeInterval duration = currentTime - self.lastTime;
if (duration > 1) {
NSString *fps = [NSString stringWithFormat:@"%.2f", (self.count / duration)];
self.FPSLabel.text = [NSString stringWithFormat:@"帧率 : %@", fps];
self.count = self.lastTime = 0;
}
}
}