Objective-C 之 利用RunLoop监测卡顿

最近学习戴铭大神的课程,其中一篇文章介绍了如何利用RunLoop监测卡顿,在此作个记录。git

1、监测卡顿的原理

文中介绍到:github

RunLoop是用来监听输入源,进行调度处理的。若是RunLoop的线程进入睡眠前方法的执行时间过长而致使没法进入睡眠,或者线程唤醒后接收消息时间过长而没法进入下一步,就能够认为是线程受阻了。若是这个线程是主线程的话,表现出来的就是出现了卡顿。bash

RunLoop有如下几种状态:服务器

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry , // 即将进入 loop,值是2^0
    kCFRunLoopBeforeTimers , // 即将处理 Timer ,值是2^1
    kCFRunLoopBeforeSources , // 即将处理 Source0 ,值是2^2
    kCFRunLoopBeforeWaiting , // 即将进入睡眠,等待 mach_port 消息,值是2^5
    kCFRunLoopAfterWaiting , // 即将处理 mach_port 消息,值是2^6
    kCFRunLoopExit , // 即将退出 loop,值是2^7
    kCFRunLoopAllActivities  // loop 全部状态改变。经过监测该值的变化,就知道runLoop的状态发生了变化
}
复制代码

而文中提到的2种线程受阻状况,它们的状态分别是:kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting。app

2、思路

根据原理,能够获得一个监测卡顿的思路:async

监测主线程RunLoop的状态,若是状态在必定时长内都是kCFRunLoopBeforeSources或者kCFRunLoopAfterWaiting,则认为卡顿。函数

步骤以下:oop

  1. 建立一个RunLoop的观察者(CFRunLoopObserverRef)
  2. 把观察者加入主线程的kCFRunLoopCommonModes模式中,以监测主线程
  3. 建立一个子线程来维护观察者
  4. 根据主线程RunLoop的状态来判断是否卡顿

3、实现方法

1. 实现代码

// 在AppDelaget.m文件添加几个属性
@interface AppDelegate ()
{
    int timeoutCount;
    CFRunLoopObserverRef runLoopObserver;
@public
    dispatch_semaphore_t dispatchSemaphore;
    CFRunLoopActivity runLoopActivity;
}
@end

@implementation AppDelegate

// 开始监测
- (void)beginMonitor {

    if (runLoopObserver) {
        return;
    }
    
    // dispatchSemaphore的知识参考:https://www.jianshu.com/p/24ffa819379c
    // 初始化信号量,值为0
    dispatchSemaphore = dispatch_semaphore_create(0); //Dispatch Semaphore保证同步
    
    // 建立一个观察者
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                              kCFRunLoopAllActivities,
                                              YES,
                                              0,
                                              &runLoopObserverCallBack,
                                              &context);
    // 将观察者添加到主线程runloop的commonModes中
    CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
    
    // 建立子线程监控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 子线程开启一个持续的loop用来进行监控
        while (1) {
            // 等待信号量:若是信号量是0,则阻塞当前线程;若是信号量大于0,则此函数会把信号量-1,继续执行线程。此处超时时间设为20毫秒。
            // 返回值:若是线程是唤醒的,则返回非0,不然返回0
            long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 20*NSEC_PER_MSEC));
//            NSLog(@"%@",@(semaphoreWait));
            if (semaphoreWait != 0) {
                if (!self->runLoopObserver) {// observer建立失败,直接返回
                    self->timeoutCount = 0;
                    self->dispatchSemaphore = 0;
                    self->runLoopActivity = 0;
                    return;
                }
                
                // 若是RunLoop执行任务的时间过长(kCFRunLoopBeforeSources),或者线程唤醒后接收消息时间过长(kCFRunLoopAfterWaiting),则认为线程受阻。
                if (self->runLoopActivity == kCFRunLoopBeforeSources || self->runLoopActivity == kCFRunLoopAfterWaiting) {
                    NSLog(@"runloop状态:%@",@(self->runLoopActivity));
                    // 60毫秒内一直保持其中一种状态,说明卡顿(20毫秒测1次,共3次)
                    if (++self->timeoutCount < 3) {
                        continue;
                    }
                    NSLog(@"发生卡顿...");
                }
            }
            self->timeoutCount = 0;
        }
    });
    
}

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
//    NSLog(@"监测到线程主线程有变化,信号量+1");
    AppDelegate *delegate = (__bridge AppDelegate*)info;
    delegate->runLoopActivity = activity;
    
    dispatch_semaphore_t semaphore = delegate->dispatchSemaphore;
    // 让信号量+1
    dispatch_semaphore_signal(semaphore);
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 开始监测
    [self beginMonitor];
    
    return YES;
}

@end

复制代码

监测到卡顿后,使用PLCrashRepoter获取堆栈信息,根据这些信息找到形成卡顿的方法。学习

#import <CrashReporter/CrashReporter.h>

// 获取数据
NSData *lagData = [[[PLCrashReporter alloc]
                    initWithConfiguration:[[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]] generateLiveReport];
// 转换成 PLCrashReport 对象
PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];
// 进行字符串格式化处理
NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];
// 将字符串上传服务器
NSLog(@"lag happen, detail below: \n %@",lagReportString);
复制代码

2. 流程说明:

为了保证子线程的同步监测,刚开始建立一个信号量是0的dispatch_semaphore。当监测到主线程的RunLoop的状态发生变化,触发回调:测试

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
复制代码

在回调里面发送信号,使信号量+1,值变为1:

dispatch_semaphore_signal(semaphore)
复制代码

dispatch_semaphore_wait接收到信号量不为0,会返回一个不为0的值(semaphoreWait),并把信号量减1:

long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 20*NSEC_PER_MSEC));
复制代码

而后触发一次监测功能,记录RunLoop状态。此时信号量是0,继续等待下一个信号量。

反复监测后,若是状态连续3次都是kCFRunLoopBeforeSources或者kCFRunLoopAfterWaiting,则断定为卡顿。

3. 关于触发卡顿的时间阈值

代码中定义了卡顿阈值是3*20ms=60ms,在线下作测试的话,这个值是合理的。可是若是在线上使用这种监测卡顿的方法,大神建议把该值设为3秒,文中介绍以下:

其实,触发卡顿的时间阈值,咱们能够根据 WatchDog 机制来设置。WatchDog 在不一样状态下设置的不一样时间,以下所示:

启动(Launch):20s; 恢复(Resume):10s; 挂起(Suspend):10s; 退出(Quit):6s; 后台(Background):3min(在 iOS 7 以前,每次申请 10min; 以后改成每次申请 3min,可连续申请,最多申请到 10min)。

经过 WatchDog 设置的时间,我认为能够把启动的阈值设置为 10 秒,其余状态则都默认设置为 3 秒。总的原则就是,要小于 WatchDog 的限制时间。固然了,这个阈值也不用小得太多,原则就是要优先解决用户感知最明显的体验问题。

参考项目:github.com/ming1016/De…

相关文章
相关标签/搜索