iOS端监控SDK探索与实践-卡顿监控

高佳杰
1.卡顿可能体现在哪些场景?
  • 点击了一个按钮过了5,6秒才有反应,并且同一个页面弹出多次。
  • 切换tab很卡
  • ......
2.为什么要进行卡顿监控?
  • 上述情况不易重现。
  • 操作路径长,日志无法准确打点
3.为什么会出现卡顿?
  • 死锁:一般会伴随 crash,可以通过 NSSetUncaughtExceptionHandler来捕获分析
  • 主线程大量 IO:大量 IO 可以在函数开始结束打点,将占用时间打到日志中。
  • 主线程大量计算:大量计算可以在函数开始结束打点,将占用时间打到日志中。
  • 大量的 UI 绘制:大量 UI 绘制一般是必现,还好办;如果是偶现的话,想加日志点都没地方,因为是慢在系统函数里面。
  • 抢锁:抢锁不好办,将锁等待时间打出来用处不大,我们还需要知道是谁占了锁。
  • ......
4.哪些指标可以认为卡顿了?
  • FPS 降低
  • CPU 占用率很高
  • 主线程 Runloop 执行了很久
5.卡顿监控的思路?
  • 起一个子线程,监控主线程Runloop执行的活动情况,如果发现有卡顿,就将堆栈 dump下来,有了堆栈我们就可以分析出主线程在什么函数哪一行卡住,在等什么锁,而这个锁又是被哪个子线程的哪个函数占用
  • 起一个子线程,检测FPS,如果连续10-20次FPS很低,就将堆栈 dump下来。(后续介绍)
  • 检测CPU占用超过100%
6.卡顿检测策略?
  • 内存级:每1秒检查一次,如果检查到主线程卡顿,就将所有线程的函数调用堆栈 dump 到内存中。
  • 文件级:如果内存 dump 的堆栈跟上次捕捉到的不一样,则 dump 到文件中;否则按照斐波那契数列将检查时间递增(1,1,2,3,5,8…)直到没有遇到卡顿或卡顿堆栈不一样。这样能够避免同一个卡顿写入多个文件的情况,也能避免检测线程围着同一个卡顿空转的情况。
7.上报策略?
  • 白名单:对于需要跟进问题的用户,可以在后台配置白名单,强制上报。
8.具体实现思路
int32_t __CFRunLoopRun()  
{
    //通知即将进入runloop
    __CFRunLoopDoObservers(KCFRunLoopEntry);

    do
    {
        // 通知将要处理timer和source
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);

        __CFRunLoopDoBlocks();  //处理非延迟的主线程调用
        __CFRunLoopDoSource0(); //处理UIEvent事件

        //GCD dispatch main queue
        CheckIfExistMessagesInMainDispatchQueue();

        // 即将进入休眠
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);

        // 等待内核mach_msg事件
        mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();

        // Zzz...

        // 从等待中醒来
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);

        // 处理因timer的唤醒
        if (wakeUpPort == timerPort)
            __CFRunLoopDoTimers();

        // 处理异步方法唤醒,如dispatch_async
        else if (wakeUpPort == mainDispatchQueuePort)
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()

        // UI刷新,动画显示
        else
            __CFRunLoopDoSource1();

        // 再次确保是否有同步的方法需要调用
        __CFRunLoopDoBlocks();

    } while (!stop && !timeout);

    //通知即将退出runloop
    __CFRunLoopDoObservers(CFRunLoopExit);
}

得到结论:NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿。

8.具体实现步骤
  • 使用CFRunLoopObserverCreate创建一个主线程runloop的观察者observer,并添加状态变化回调函数,记录runloop的状态
  • 将observer观察者添加到主线程的runloop中
  • 使用GCD开启一个子线程监控主线程runloop两个kCFRunLoopBeforeSources(处理)和kCFRunLoopAfterWaiting(休眠)的间隔时长。
  • 记录时长超过自定义阀值的堆栈信息。
8.完整代码