NSRunloop卡顿监控

作者: Jeffery91 | 来源:发表于2016-07-01 21:33 被阅读680次

    说说界面卡顿是怎么产生的?
    先说屏幕,苹果移动设备屏幕,即显示器的刷新频率是60HZ,这是硬件设备决定的,无论使用者感觉卡还是不卡,都会按照这个频率进行刷新。显示器显示的内容是由显卡渲染的,显卡渲染一帧并显示到显示器上的时间点,程序可以通过CADisplayLink捕获。由于iOS设备都开启了垂直同步,显卡总是等到显示器发出垂直同步信号后再开始渲染下一帧。如果两次垂直同步信号之间,即16.7ms内,渲染数据没有准备好,那么这一帧数据就会丢失,显示器刷新的仍然是上一帧的数据,造成掉帧卡顿。

    那么什么情况下会导致没有准备好渲染数据呢?
    这需要考虑渲染数据从哪来。App主线程在CPU中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后CPU会将计算好的内容提交到GPU去,由GPU进行变换、合成、渲染。随后GPU会把渲染结果提交到帧缓冲区去。CPU和GPU不论哪个阻碍了显示流程,都会造成掉帧现象。

    我们在程序中能做的只有监控CPU了,GPU无能为力,而且通过观察instruments,会发现除了离屏渲染,其他情况下GPU并不是瓶颈,平时开发中尽量避免即可。主线程上的CPU工作都是在RunLoop中进行的,从下面的伪代码可以看到主要计算工作都在kCFRunLoopAfterWaiting和下一次kCFRunLoopBeforeWaiting之间。

    setupThisRunLoopRunTimeoutTimer(); //by GCD timer
    __CFRunLoopDoObservers(KCFRunLoopEntry);
    do
    {
    __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
    __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
    
    __CFRunLoopDoBlocks(); //处理非延迟的主线程调用
    __CFRunLoopDoSource0(); //处理UIEvent事件
    
    CheckIfExistMessagesInMainDispatchQueue(); //检查GCD是否有在MainDispatchQueue中要执行的事件
    __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
    
    mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts(); //等待内核mach_msg事件
    //Zzz...
    __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
    
    //处理唤醒事件
    if (wakeUpPort == timerPort) //timer唤醒
        __CFRunLoopDoTimers();
    else if (wakeUpPort == mainDispatchQueuePort) //GCD唤醒
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
    else //端口唤醒,如网络请求回来
        __CFRunLoopDoSource1();
    
    __CFRunLoopDoBlocks();
    } while (!stop && !timeout);
    __CFRunLoopDoObservers(CFRunLoopExit);
    

    所以,可以将监控的RunLoop的运行时间,设置为kCFRunLoopAfterWaiting开始到下一次kCFRunLoopBeforeWaiting结束。这虽然和系统意义上一次完整的RunLoop不同(系统意义上的一次RunLoop,应该和AutoreleasePool的重建时机一样,即kCFRunLoopBeforeWaiting到下一次kCFRunLoopBeforeWaiting之间),但是runloop休眠的时间肯定不能认为是卡顿。粗略计算一下,以运行时低于40帧为卡,则会掉20帧,卡住的时间约为320ms。可以假设如果runloop执行超过了0.3,主线程无法将计算好的内容提交给 GPU,会造成卡顿。

    综上,为主线程的 RunLoop 添加一个 Observer ,来检测 RunLoop 的运行情况。

    CFRunLoopObserverContext context = {0, NULL, NULL, NULL};
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                        kCFRunLoopAllActivities,
                                                        YES,
                                                        0,
                                                        &runLoopObserverCallBack,
                                                        &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    

    在回调中,使用mach_absolute_time()记录kCFRunLoopAfterWaiting的时间点,在下一次kCFRunLoopBeforeWaiting时计算RunLoop的运行时间,如果超时,可以根据需求处理,比如dump函数堆栈,并上传监控服务器等。示例中使用的是断言处理。

    static const NSTimeInterval kRunLoopThreshold = 0.3;
    static uint64_t kStartTime = 0;
    static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
    {
      switch (activity) {
        case kCFRunLoopAfterWaiting:
            kStartTime = mach_absolute_time();
            break;
        case kCFRunLoopBeforeWaiting:
            if (kStartTime != 0 ) {
                uint64_t elapsed = mach_absolute_time() - kStartTime;
                mach_timebase_info_data_t timebase;
                mach_timebase_info(&timebase);
                NSTimeInterval duration = elapsed * timebase.numer / timebase.denom / 1e9;
                if (duration > kRunLoopThreshold) { 
                    assert(0);
                }
            }
            break;
        default:
            break;
        }
    }
    

    上述计算中,在kCFRunLoopBeforeWaiting时每次都需要将mach_absolute_time()的时间转换成秒,会比较浪费,可以通过context参数传进来。

    相关文章

      网友评论

        本文标题:NSRunloop卡顿监控

        本文链接:https://www.haomeiwen.com/subject/ruqqjttx.html