iOS卡顿监控原理分析

2020-04-22  本文已影响0人  哦小小树

0x01卡顿产生原因

概述:
CPU与GPU两者总共耗时超出视屏刷新帧最小间隔时间T。

如果1s刷新正常按照60帧计算,每帧时间大概为16.7ms。如果出现T多次超出这个范围,导致我们1s实际显示帧数远小于60,我们可以定义为产生了卡顿。


0x02 Runloop与卡顿关系

为什么UI刷新与Runloop有关?

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 此回调注册在0xa0kCFRunLoopBeforeWaiting
而上面的回调就会触发UI刷新;
函数的调用栈大概为:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

Runloop的执行图网上已经有很多就不引用了,当下记录下大概的流程。

 ----------BeforeTimers
 ----------BeforeSources
 ++++++++++ 处理Block
 ++++++++++ 处理Source0(非port),手动处理
 ++++++++++ 如果Source0处理成功,则处理Block
 ==== 如果有Source1事件,直接跳转到HandleMSG

 ----------BeforeWaitting
 ==== 
  唤醒条件【即有任务待处理】:
  runloop时间超时,timer触发,Port有消息出发, 被手动唤醒
 ====
 ----------AfterWaiting

 ==== HandleMSG【被唤醒后需要处理的任务】
 ++++++++++++ 处理计时器事件
 ++++++++++++ 处理主异步到主队列的消息
 ++++++++++++ 处理source1事件
 ++++++++++++ 处理block事件

从上面任务执行流程可以看出:其主要执行任务的时机为:BeforeSourcesAfterWaiting

  1. 对于任务比较多,导致CPU+GPU处理不完的卡顿情形可以通过监听这两个通知执行时间是否超时来达到卡顿监控。

  2. 对于屏幕静止是卡顿,由于一直是处于BeforeWaiting状态,我们可以通过ping主线程的方式验证主线程是否卡顿。

source0:
Port的处理事件,意思是这个事件不是系统或其他线程通过port发送给你的,而是有用户操作了某些东西产生的事件。
理解为用户操作;比如点击,触摸。

source1:来自系统内核或其他进程或线程的事件。它可以主动唤醒休眠状态的RunLoop
我们理解为系统做的任务。理解为非用户操作。


0x03 卡顿处理方案

交互流畅性

逻辑如下

  1. 定义信号量
  2. 监听主线程runloop活动状态, 当状态改变时发出一个信号量
  3. 创建一个循环监控任务
    3.1 等待信号量,并设置一个可以接收的超时时间T
    3.2 如果超时一定时间,且Runloop状态为BeforeSources或者AfterWaiting则记录一次,当连续超过5次,记录一次卡顿,此时打印当前调用栈信息.
  4. 将任务异步添加到一个串行队列中【开一条线程执行任务】

代码实现如下

// runloop观察回调
static void lxdRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void * info) {
// 记录runloop状态
    SHAREDMONITOR.currentActivity = activity;
// 发出信号, 信号量加1,正在阻塞的方法dispatch_semaphore_wait可以得到调用
    dispatch_semaphore_signal(SHAREDMONITOR.semphore);
}

注册完Runloop的观察者之后,添加以下代码

dispatch_async(lxd_fluecy_monitor_queue(), ^{
        while (SHAREDMONITOR.isMonitoring) {    // 判断当前是否正在监控卡顿
            // 如果等待了timeout时间后还没收到信号,就会停止等待,接着往下执行,此时waitTime不为0;如果在超时前等到,则waitTime为0
            long waitTime = dispatch_semaphore_wait(self.semphore, dispatch_time(DISPATCH_TIME_NOW, lxd_wait_interval));
            if (waitTime != LXD_SEMPHORE_SUCCESS) {    // 超时
                if (!SHAREDMONITOR.observer) {  // 如果当前没在监控就直接停掉
                    SHAREDMONITOR.timeOut = 0;
                    [SHAREDMONITOR stopMonitoring];
                    continue;
                }
/* 超时,说明正在等runloop的下一个活动状态,结果发现等的超时了还没有等到下一个状态,而此时还是上一个状态。
那就可以判断为:此活动状态执行的时间太长了。

为什么不用BeforeWaiting呢? 
因为如果页面静止时,Runloop会一直保持在BeforeWaiting状态,此时会一直超时,就会误判,所以需要单独判静止页面卡顿
*/
                if (SHAREDMONITOR.currentActivity == kCFRunLoopBeforeSources || SHAREDMONITOR.currentActivity == kCFRunLoopAfterWaiting) {
                    if (++SHAREDMONITOR.timeOut < 5) {  // 连续5次,每次200ms,共1s可以判断为卡顿
                        continue;
                    }
                    [LXDBacktraceLogger lxd_logMain];  // 打印调用栈信息
                    [NSThread sleepForTimeInterval: lxd_restore_interval]; // 休眠以下,用来打印当前调用栈,不至于突然执行其他任务,打印了其他调用栈
                }
            }
            SHAREDMONITOR.timeOut = 0;  // 超时次数归0
        }
    });
静止页面卡顿

考虑到上面静止卡顿就无法进行处理。此时可以考虑换个思路:
通过创建一个状态,然后异步到主线程去修改这个状态。如果再指定时间内发现状态被修改,那么主线程就没有卡顿,否则就卡顿。
逻辑如下

  1. 创建一个任务,标记一个变量timeout = YES, 并将任务以异步的方式,放到一个串行队列【开个线程】
  2. 异步到主线程中,修改这个变量为NO
  3. 子线程等待一定时间判断timeout值,如果有被修改,则继续循环,否则标记为超时,打印调用栈信息

代码实现

    dispatch_async(lxd_event_monitor_queue(), ^{
        while (SHAREDMONITOR.isMonitoring) {  // 监控中
            if (SHAREDMONITOR.currentActivity == kCFRunLoopBeforeWaiting) {    // 保证是BeforeWaiting状态
                __block BOOL timeOut = YES;  // 定义个超时变量
                dispatch_async(dispatch_get_main_queue(), ^{
/*
异步到主线程,就会放到当前主队列里面,按序执行,如果在设置的时间内执行到,说明得到了刷新。
*/
                    timeOut = NO;  // 异步到主线程去修改
                    // 发出信号,告诉子线程,可以执行下一次循环了
                    dispatch_semaphore_signal(SHAREDMONITOR.eventSemphore);
                });
                [NSThread sleepForTimeInterval: lxd_time_out_interval];  // 子线程等待固定时间
                if (timeOut) {  // 超时,打印调用栈信息
                    [LXDBacktraceLogger lxd_logMain];
                }
                // 等待,知道收到信号,然后再执行,避免循环调用太快
                dispatch_wait(SHAREDMONITOR.eventSemphore, DISPATCH_TIME_FOREVER);
            }
        }
    });
CPU负载过高

如果CPU负载过高,就算不会崩溃也是个优化点

#include <mach/mach.h>

// 获取 CPU 使用率
+ (float)cpuUsageForApp {
    
    thread_act_array_t threads;
    mach_msg_type_number_t threadCount = 0;
    const task_t thisTask = mach_task_self();
    kern_return_t kr = task_threads(thisTask, &threads, &threadCount);
    if (kr != KERN_SUCCESS) {
        return -1;
    }
    float totalCpuUsage = 0;
    for (int i = 0; i < threadCount; i++) {
        thread_info_data_t threadInfo;
        thread_basic_info_t threadBaseInfo;
        mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;
        if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
            threadBaseInfo = (thread_basic_info_t)threadInfo;
            if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {
                float_t cpuUsage = threadBaseInfo->cpu_usage / 10.0;
                totalCpuUsage += cpuUsage;
            }
        }
    }
    return totalCpuUsage;
}
FPS计算

逻辑如下

  1. 构建一个CADisplayLink添加到主线程,添加方法
  2. 在方法中记录时间1s内执行多少次,计算出多少帧率
{
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
    [link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

static int count = 0;
static NSTimeInterval lastTime = 0;//时间间隔

- (void)tick:(CADisplayLink *)link {
    if (lastTime == 0) {
        lastTime = link.timestamp;
        return;
    }
    
    count += 1;
    NSTimeInterval delta = link.timestamp - lastTime;
    if (delta >= 1) {
        lastTime = link.timestamp;
        float fps = count / delta;
        NSLog(@"帧率:%.0f",fps);
        count = 0;
    }
}

缺点:
只能看到卡顿无法定位卡帧位置


总结

卡顿监控就是利用Runloop在发出BeforeWaiting通知后触发UI刷新的特点,再依靠任务执行是否超出阈值来判断卡顿的。

参考项目:
https://github.com/chenshuangsmart/FluecyMonitor.git

上一篇 下一篇

猜你喜欢

热点阅读