你们好,第一次在掘金这个平台写东西。若有错误,但愿指出。
最近发现网上常常被人讨论的APP在线上状态如何检测到主线程的卡顿状况,我也稍微了解了一下,前段时间就在一个博主的文章里看到一篇有部分讲解这个问题的,听说美团用的也是这种方案,具体不得而知,而后我发现网上关于这种问题的实现方案都十分相似,若是屏幕前的你尚未意识过这个问题,那就请听我往下分析这个网上经常使用的检测方案:bash
利用runloop的检测方案异步
关于runloop是什么我就很少说了,由于网上有不少关于这个的文章,最推荐的仍是YYKit的做者博客上那篇。
我要拿出来注意的是 runloop 的状态:async
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};复制代码
网上热议的是利用 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 这两个状态之间的耗时进行判断是否有太多事件处理致使出现了卡顿,下面直接上代码:函数
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
PingConfig *object = (__bridge PingConfig*)info;
// 记录状态值
object->activity = activity;
// 发送信号
dispatch_semaphore_t semaphore = object->semaphore;
dispatch_semaphore_signal(semaphore);
}复制代码
上面这些是监听runloop的状态而写的回调函数oop
- (void)registerObserver
{
PingConfig *config = [PingConfig new];
// 建立信号
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
config->semaphore = semaphore;
CFRunLoopObserverContext context = {0,(__bridge void*)config,NULL,NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
__block uint8_t timeoutCount = 0;
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 假定连续5次超时50ms认为卡顿(固然也包含了单次超时250ms)
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (st != 0)
{
// NSLog(@"循环中--%ld",config->activity);
if (config->activity==kCFRunLoopBeforeSources || config->activity==kCFRunLoopAfterWaiting)
{
if (++timeoutCount < 5){
continue;
}else{
NSLog(@"卡顿了");
}
}
}
timeoutCount = 0;
}
});
}复制代码
如今我解读一下这段代码:测试
@interface PingConfig : NSObject
{
@public
CFRunLoopActivity activity;
dispatch_semaphore_t semaphore;
}
@end复制代码
恩,只有这么多足矣。通过测试,的确能够检测到主线程的卡顿现象,不得不佩服大佬们的方案。
可是在一次测试中,发现当主线程卡在界面还没有彻底显示前,这个方案就检测不出来卡顿了,好比我将下面的代码放在B控制器中:优化
dispatch_semaphore_t t = dispatch_semaphore_create(0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"----");
dispatch_semaphore_signal(t);
});
dispatch_semaphore_wait(t, DISPATCH_TIME_FOREVER);复制代码
上面是一段有问题的代码,将致使主线程的持续堵塞,若是咱们在这段代码放在B控制器的ViewDidLoad方法中(ViewWillAppear一样),这样运行后,当你但愿push到B控制器时,项目将在上一个界面彻底卡住,而且没法用上面的方案检测到,并且CPU及内存都显示正常:ui
具体缘由我想了一下,因为runloop在处理完source0或者source1后,好比界面的跳转也是执行了方法,具体有没有用到source0这不重要,可是后面会紧接着进入准备睡眠(kCFRunLoopBeforeWaiting)的状态,然而此时线程的阻塞致使runloop的状态也被卡住没法切换,这样也就致使在那段检测代码中没法进入条件,从而检测不出来。
可是话说回来,APP在静止状态(保持休眠)和刚刚那种卡死状态都会使runloop维持在 kCFRunLoopBeforeWaiting状态,这样咱们就没法在那段代码中增长判断来修复,由于没法知道究竟是真的静止没有操做仍是被阻塞住,我也没找到线程的阻塞状态属性,若是你发现这个属性,那么就可使用那个属性来判断。可是我也得说下在没找到那个属性时个人检测方案:spa
个人检测方案线程
先上代码:
dispatch_queue_t serialQueue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, serialQueue);
dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 0.25 * NSEC_PER_SEC, 0);
__block int8_t chokeCount = 0;
dispatch_semaphore_t t2 = dispatch_semaphore_create(0);
dispatch_source_set_event_handler(self.timer, ^{
if (config->activity == kCFRunLoopBeforeWaiting) {
static BOOL ex = YES;
if (ex == NO) {
chokeCount ++;
if (chokeCount > 40) {
NSLog(@"差很少卡死了");
dispatch_suspend(self.timer);
return ;
}
NSLog(@"卡顿了");
return ;
}
dispatch_async(dispatch_get_main_queue(), ^{
ex = YES;
dispatch_semaphore_signal(t2);
});
BOOL su = dispatch_semaphore_wait(t2, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (su != 0) {
ex = NO;
};
}
});
dispatch_resume(self.timer);复制代码
解释一下个人方案:
我写的这段解决方案中的示例代码只是用来演示,具体是原理能够你们尽情在此基础上优化,目前在个人项目中能够正常检测到以前那种阻塞形成的APP卡死现象,若是你发现有更好的检测方案,但愿能告诉我,谢谢!