监控FPS

FPS :Frames Per Second 的简称缩写,意思是每秒传输帧数,FPS值越低就越卡顿,所以这个值在一定程度上可以衡量应用在图像绘制渲染处理时的性能。iOS系统中正常的屏幕刷新率为60Hz(60次每秒)。
通过CADisplayLink实现FPS监控,CADisplayLink可以以屏幕刷新的频率调用指定selector,也就是说每次屏幕刷新的时候就调用selector,那么只要在selector方法里面统计每秒这个方法执行的次数,通过次数/时间就可以得出当前屏幕的刷新率了。
可通过YYFPSLabel或者KMCGeigerCounter进行监控,但是前者比较轻量级。
YYFPSLabelKMCGeigerCounter

通过RunLoop监控检查卡顿

RunLoop监控原理:通过RunLoop知道主线程上都调用了哪些方法,通过监听 nsrunloop 的状态,知道调用方法是否执行时间过长,从而判断出是否卡顿。
RunLoop 原理来监控卡顿的话,就是要关注这两个阶段。RunLoop 在进入睡眠之前和唤醒后的两个 loop状态定义的值分别是 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting,也就是要触发 Sources回调和接收mach_port消息两个状态。

 

监听RunLoop

  • 1.需要创建一个CFRunLoopObserverContext观察者,然后将观察者runLoopObserver添加到主线程 RunLoop的common模式下观察

 

CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);
  • 2.创建一个持续的子线程专门用来监控主线程的RunLoop状态

 

// 创建子线程监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 子线程开启一个持续的 loop 用来进行监控
    while (YES) {
        long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
        if (semaphoreWait != 0) {
            if (!runLoopObserver) {
                timeoutCount = 0;  // 超时次数
                dispatchSemaphore = 0; // dispatch_semaphore_t 信号量
                runLoopActivity = 0;  // CFRunLoopActivity RunLoop原始状态kCFRunLoopEntry
                return;
            }
            //kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting这两个状态能够检测到是否卡顿
            if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
                // 将堆栈信息上报服务器的代码放到这里,包括timeoutCount操作
            } 
        }
        timeoutCount = 0;
    }
});

NSEC_PER_SEC代表的是触发卡顿的时间阈值,单位是秒。可以看到,我们把这个阀值设置成了 3 秒。

获取卡顿的方法堆栈信息

方法1:直接调用系统函数方法的主要思路是:用 signal 进行错误信息的获取
性能消耗小。但是,它只能够获取简单的信息,也没有办法配合 dSYM 来获取具体是哪行代码出了问题,而且能够获取的信息类型也有限。适用于观察大盘统计卡顿情况、而不是想要找到卡顿原因的场景。

 

static int s_fatal_signals[] = {
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGSEGV,
    SIGTRAP,
    SIGTERM,
    SIGKILL,
};

static int s_fatal_signal_num = sizeof(s_fatal_signals) / sizeof(s_fatal_signals[0]);

void UncaughtExceptionHandler(NSException *exception) {
    NSArray *exceptionArray = [exception callStackSymbols]; // 得到当前调用栈信息
    NSString *exceptionReason = [exception reason];       // 非常重要,就是崩溃的原因
    NSString *exceptionName = [exception name];           // 异常类型
}

void SignalHandler(int code)
{
    NSLog(@"signal handler = %d",code);
}

void InitCrashReport()
{
    // 系统错误信号捕获
    for (int i = 0; i < s_fatal_signal_num; ++i) {
        signal(s_fatal_signals[i], SignalHandler);
    }
    
    //oc 未捕获异常的捕获
    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}

int main(int argc, char * argv[]) {
    @autoreleasepool {
        InitCrashReport();
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}

方法2:利用PLCrashReporter 能够定位到问题代码的具体位置,而且性能消耗也不大

 

NSData *lagData = [[[PLCrashReporter alloc]
                                          initWithConfiguration:[[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]] generateLiveReport];
PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];
NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];
NSLog(@"lag happen, detail below: \n %@",lagReportString);