关于线上检测主线程卡顿的问题

你们好,第一次在掘金这个平台写东西。若有错误,但愿指出。
最近发现网上常常被人讨论的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;
        }
    });
}复制代码

如今我解读一下这段代码:测试

  1. PingConfig 只是我随便写的一个用来存储runloop的状态和信号量的自定义类,其中的结构以下:
    @interface PingConfig : NSObject
    {
     @public
     CFRunLoopActivity activity;
     dispatch_semaphore_t semaphore;
    }
    @end复制代码
    恩,只有这么多足矣。
  2. APP启动时我能够进入 registerObserver 方法,其中首先我建立一个记录信息的类PingConfig实例,而后建立一个信号,而且保存在这个PingConfig实例中(其实只是为了方便拿到)。
  3. 接下来我建立了一个观察者监测主线程的 runloop,它会在主线程runloop状态切换时进行回调。
  4. 开启一个子线程,而且在里面进行一个 while 循环,在 循环的开始处 wait 一个信号量,而且设置超时为 50毫秒,失败后会返回一个非0数,成功将会返回0,这时候线程会阻塞住等待一个信号的发出。
  5. 若是runloop状态正常切换,那么就会进入回调函数,在回调函数中咱们发出一个信号,而且记录当前状态到PingConfig实例中,下面的判断语句中发现为0,timeoutCount自动置为0,一切正常。
  6. 当主线程出现卡顿,while循环中的信号量再次等待,可是回调函数没有触发,从而致使等待超时,返回一个非0数,进入判断句后,咱们再次判断状态是否处于 kCFRunLoopBeforeSources 或 kCFRunLoopAfterWaiting,若是成立,timeoutCount+1。
  7. 持续五次runloop不切换状态,说明runloop正在处理某个棘手的事件没法休息且不更新状态,这样while循环中的信号量超时会一直发生,超过五次后咱们将判定主线程的卡顿并上传堆栈信息。

通过测试,的确能够检测到主线程的卡顿现象,不得不佩服大佬们的方案。
可是在一次测试中,发现当主线程卡在界面还没有彻底显示前,这个方案就检测不出来卡顿了,好比我将下面的代码放在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

QQ20170930-153549@2x.png
QQ20170930-153549@2x.png

具体缘由我想了一下,因为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);复制代码

解释一下个人方案:

  1. 开启一个异步队列,而且建立一个定时器,时间我设置的是0.25秒,具体时间随你本身,这个时间是用来检测卡死的持续时间。
  2. 在定时器外面我也一样建立了一个用来同步的信号量,这个不解释了,不会的就去看一下信号量的使用方式。进入定时器的回调后,我设置了一个静态变量来记录主队列是否执行完成。
  3. 咱们判断当前runloop的状态是否为kCFRunLoopBeforeWaiting,因此这个方案是用来弥补前面那个方案,若是主线程此时没有阻塞住,咱们在这里向main Queue抛一个block,看它是否可以成功执行,若是成功执行,说明主线程没有阻塞住,若是已经被阻塞住,那我抛过去的block是确定不会被执行的。
  4. 下面的代码就是一些辅助操做,当信号量超过50毫秒,抛给主线程的block没有执行,那么说明此时就有一些阻塞了,返回一个非0数,并设置 ex为NO,从而在下一次定时器回调到来时进行上报。

我写的这段解决方案中的示例代码只是用来演示,具体是原理能够你们尽情在此基础上优化,目前在个人项目中能够正常检测到以前那种阻塞形成的APP卡死现象,若是你发现有更好的检测方案,但愿能告诉我,谢谢!

相关文章
相关标签/搜索