NSRunloop卡顿监控

说说界面卡顿是怎么产生的?
先说屏幕,苹果移动设备屏幕,即显示器的刷新频率是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卡顿监控)