美文网首页
ios-界面卡顿监测

ios-界面卡顿监测

作者: Harry__Li | 来源:发表于2022-01-25 16:26 被阅读0次

    现在对于app的要求越来越高,并不是像以前我们只要把功能需求做出来就行。还要求我们对app做各种优化,写出高质量的代码。所以对于界面的流畅度的监测就非常有必要了。那应该如何监测呢?

    FPS的监测
    • 原理:CADisplayLink是一个特殊的定时器,它的调用执行和GPU的渲染有关,我们常常说的60刷和120hz刷屏。也就以为这,默认情况下CADisplayLink每秒会调用60次或者120次。我们就可以计算它在一秒内调用了多少次,来看界面的流畅度。正常情况下50<fps 肉眼看不出来卡顿,55以上就很流畅,说明你已经做的很好了。下面我们来看代码,具体是如何实现的。
    //初始化创建
    CADisplayLink *link=[CADisplayLink displayLinkWithTarget:self selector:@selector(displayAction:)];
    //添加到主线程中去
        [link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    
    //通记录回调次数和相隔时间 计算fps
    -(void)displayAction:(CADisplayLink *)link{
        //记录上次的时间
        static NSTimeInterval lastTime = 0;
        //记录回调了多少次
        static NSInteger frameCount = 0;
        if (lastTime==0) {
            lastTime=link.timestamp;
            return;
        }
        frameCount++;
        NSTimeInterval duration=link.timestamp-lastTime;
        //刷新间隔1秒
        if (duration>=1) {
            NSInteger fps=frameCount/duration;
            frameCount=0;
            lastTime=link.timestamp;
        }
    }
    
    • 优缺点:通过上面的代码,可以看到界面帧率。越小说明界面越不流畅。但它的作用也仅仅只是这样了。即使知道了卡顿,我们还要自己去查找,无法快速的定位到底是那个方法引起的卡顿。
    RunLoop监听应用程序卡顿

    在前面我们讲过runloop那个非常烂大街的流程图,这里就不放了。
    KCFRunLoopEntyr开始runloop-->timer--> kCFRunLoopBeforeSources(开始处理事件)-->kCFRunLoopBeforeWaiting(事件处理完了,开始休眠)--> kCFRunLoopAfterWaiting(被叫醒)-->然后开始回到最开的步骤 重新实现流程
    原理:创建一个子线程进行循环监测,每次检测时设置为YES,然后派发任务切换到主线程,将标记位置设置为NO,如果没有设置成NO,说明主线程卡顿了。关键代码如下:

    #import <Foundation/Foundation.h>
    #define SHAREDMONITOR [LXDAppFluecyMonitor sharedMonitor]
    /*!
     *  @brief  监听UI线程卡顿
     */
    @interface LXDAppFluecyMonitor : NSObject
    + (instancetype)sharedMonitor;
    - (void)startMonitoring;
    - (void)stopMonitoring;
    @end
    
    #import "LXDAppFluecyMonitor.h"
    
    #define LXD_DEPRECATED_POLLUTE_MAIN_QUEUE
    
    @interface LXDAppFluecyMonitor ()
    @property (nonatomic, assign) int timeOut;
    @property (nonatomic, assign) BOOL isMonitoring;
    @property (nonatomic, assign) CFRunLoopObserverRef observer;
    @property (nonatomic, assign) CFRunLoopActivity currentActivity;
    @property (nonatomic, strong) dispatch_semaphore_t semphore;
    @property (nonatomic, strong) dispatch_semaphore_t eventSemphore;
    @end
    
    #define LXD_SEMPHORE_SUCCESS 0
    static NSTimeInterval lxd_restore_interval = 5;
    static NSTimeInterval lxd_time_out_interval = 1;
    static int64_t lxd_wait_interval = 200 * NSEC_PER_MSEC;
    
    /*!
     *  @brief  监听runloop状态在after waiting和before sources之间
     */
    static inline dispatch_queue_t lxd_fluecy_monitor_queue() {
        static dispatch_queue_t lxd_fluecy_monitor_queue;
        static dispatch_once_t once;
        dispatch_once(&once, ^{
            lxd_fluecy_monitor_queue = dispatch_queue_create("com.sindrilin.lxd_monitor_queue", NULL);
        });
        return lxd_fluecy_monitor_queue;
    }
    
    #define LOG_RUNLOOP_ACTIVITY 0
    static void lxdRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void * info) {
        SHAREDMONITOR.currentActivity = activity;
        dispatch_semaphore_signal(SHAREDMONITOR.semphore);
    #if LOG_RUNLOOP_ACTIVITY
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"runloop entry");
                break;
            case kCFRunLoopExit:
                NSLog(@"runloop exit");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"runloop after waiting");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"runloop before timers");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"runloop before sources");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"runloop before waiting");
                break;
            default:
                break;
        }
    #endif
    };
    
    @implementation LXDAppFluecyMonitor
    
    #pragma mark - Singleton override
    + (instancetype)sharedMonitor {
        static LXDAppFluecyMonitor * sharedMonitor;
        static dispatch_once_t once;
        dispatch_once(&once, ^{
            sharedMonitor = [[super allocWithZone: NSDefaultMallocZone()] init];
            [sharedMonitor commonInit];
        });
        return sharedMonitor;
    }
    
    + (instancetype)allocWithZone: (struct _NSZone *)zone {
        return [self sharedMonitor];
    }
    
    - (void)dealloc {
        [self stopMonitoring];
    }
    
    - (void)commonInit {
        self.semphore = dispatch_semaphore_create(0);
    }
    #pragma mark - Public
    - (void)startMonitoring {
     if (_isMonitoring) { return; }
        _isMonitoring = YES;
        CFRunLoopObserverContext context = {
            0,
            (__bridge void *)self,
            NULL,
            NULL
        };
        //创建监听者
        _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &lxdRunLoopObserverCallback, &context);
        //监听主RunLoop
        CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    dispatch_async(lxd_event_monitor_queue(), ^{
        NSLog(@"%@",[NSThread currentThread]);
        while (SHAREDMONITOR.isMonitoring) {
            //主线程的RunLoop 即将进入休眠
            if (SHAREDMONITOR.currentActivity == kCFRunLoopBeforeWaiting) {
                //默认超时
                __block BOOL timeOut = YES;
                NSLog(@"0");
                
                dispatch_async(dispatch_get_main_queue(), ^{
                    //切换到主线程 执行任务
                    //若主线程没有出现卡顿 能正常执行任务 将timeOut设置为NO
                    //若主线程出现卡顿 不能能正常执行任务
                   timeOut = NO;
                    //发送信号量 +1
                   dispatch_semaphore_signal(SHAREDMONITOR.eventSemphore);
                   NSLog(@"1");
                });
                
                NSLog(@"2");
                //当前子线程休眠1秒钟
                [NSThread sleepForTimeInterval: lxd_time_out_interval];
                NSLog(@"3");
                //超时打印函数调用栈
                if (timeOut) {
                   NSLog(@"4");
                   [LXDBacktraceLogger lxd_logMain];
                }
                NSLog(@"5");
                //释放信号量 -1 此时的信号量为-1<0 下面的逻辑不会执行 循环依然执行
                dispatch_wait(SHAREDMONITOR.eventSemphore, DISPATCH_TIME_FOREVER);
                NSLog(@"6");
            }
        }
    });
    }
    - (void)stopMonitoring {
        if (!_isMonitoring) { return; }
        _isMonitoring = NO;
        
        CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
        CFRelease(_observer);
        _observer = nil;
    }
    @end
    

    上面一堆的代码,主要实现的逻辑方法在startMonitoring中

    • 1、首先判断_isMonitoring,当_isMonitoring为YES时,说明主线程一直处于卡顿状态。
    • 2、_isMonitoring为NO时,表示已派发任务到主线程。这时我们把_isMonitoring设置为YES。
      -3、 创建监听这,监听主runloop。
    • 4、创建子线程lxd_fluecy_monitor_queue。while判断_isMonitoring,为0,表示在指定时间内接收到主线程发出的信号;非0,表示在指定时间内没有接收到主线程发出的信号,主线程可能在执行耗时任务,有可能造成应用程序的卡顿
      -5、判断主runloop即将进入休眠,这时候切换到主线程 执行任务。同时我们让当前的子线程休眠一秒。这里为什么要休眠呢?这时因为,切换到主线程 设置timeout为NO,如果在我们设置的时间内能切换成功,就判断他不卡顿。如若1秒以后timeout还是yes,证明主线程在执行耗时操作。没有办法去执行 timeOut = NO;。这个时候我们就要记录卡顿的日志[LXDBacktraceLogger lxd_logMain];
      -6.最后 信号量也没办法释放,他会一直等待主线程切换执行,发出信号成功以后才能释放。
    使用Instrument工具实时监测App

    使用Time Profiler
    修改项目为debug模式,然后如下图设置


    截屏2022-01-25 下午4.18.57.png

    按步骤打开product-->profile-->time


    截屏2022-01-25 下午4.24.13.png

    设置call tree,显示代码位置。

    以上就完成了界面卡顿监测。

    相关文章

      网友评论

          本文标题:ios-界面卡顿监测

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