美文网首页IOS知识积累
iOS:卡顿延迟监测、界面启动耗时、界面FPS

iOS:卡顿延迟监测、界面启动耗时、界面FPS

作者: 丶墨墨丶 | 来源:发表于2019-08-17 13:34 被阅读0次

    卡顿延迟监测

    App在线运行的时候发生了卡顿,是很难定位卡顿原因的。

    一般界面卡顿原因:
    1.死锁:主线程拿到锁 A,需要获得锁 B,而同时某个子线程拿了锁 B,需要锁 A,这样相互等待就死锁了。
    2.抢锁:主线程需要访问 DB,而此时某个子线程往 DB 插入大量数据。通常抢锁的体验是偶尔卡一阵子,过会就恢复了。
    3.主线程大量 IO:主线程为了方便直接写入大量数据,会导致界面卡顿。
    4.主线程大量计算:算法不合理,导致主线程某个函数占用大量 CPU。
    5.大量的 UI 绘制:复杂的 UI、图文混排等,带来大量的 UI 绘制。

    一个相对比较有用的办法是做一个常驻线程,定时抓取主线程的运行时状态,当主线程的运行时状态在几个周期里总是处于同一个状态/或同一类状态时,则大概率认为发生了卡顿,此时使用CrashReporter这个第三方组件模拟一个crash获取到对应的call stack就好对问题进行跟进了。

    至于程序员怎么拿到call stack进行分析,则各有各的办法,有些会自己搭建一个后台服务,将call stack信息做上传,我们就不想搞那么多东西,直接上传到Bugfender了,实时查看。
    .h

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface NKDelayedMonitor : NSObject
    
    + (instancetype)sharedInstance;
    
    // 阀时(单位ms)
    @property (nonatomic, assign) NSInteger gateTime;
    
    - (void)startOnlineMonitor;
    - (void)stopOnlineMonitor;
    
    - (void)startOfflineMonitor;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    .m

    #import "NKDelayedMonitor.h"
    #import "BSBacktraceLogger.h"
    #import "UIViewController+FPS.h"
    
    @interface NKDelayedMonitor ()
    
    @property (nonatomic, assign) int timeoutCount;
    @property (nonatomic, assign) CFRunLoopObserverRef observer;
    @property (nonatomic, strong) dispatch_semaphore_t semaphore;
    @property (nonatomic, assign) CFRunLoopActivity activity;
    @end
    
    @implementation NKDelayedMonitor
    
    + (instancetype)sharedInstance
    {
        static id instance = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            instance = [[self alloc] init];
        });
        return instance;
    }
    
    - (instancetype)init {
        if (self = [super init]) {
            _gateTime = 250;// 默认门阀时间为250ms
        }
        return self;
    }
    
    static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
    {
        NKDelayedMonitor *moniotr = (__bridge NKDelayedMonitor*)info;
        moniotr.activity = activity;
        dispatch_semaphore_t semaphore = moniotr.semaphore;
        dispatch_semaphore_signal(semaphore);
    }
    
    - (void)setGateTime:(NSInteger)gateTime {
        if (_gateTime && _gateTime >= 100) {
            _gateTime = gateTime;
        }
    }
    
    - (void)stopOnlineMonitor
    {
        if (!_observer)
            return;
        CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
        CFRelease(_observer);
        _observer = NULL;
    }
    
    - (void)startOnlineMonitor
    {
        if (_observer)
            return;
        
        NSInteger countTimes = _gateTime / 50;
        
        // 信号,Dispatch Semaphore保证同步
        _semaphore = dispatch_semaphore_create(0);
        
        // 注册RunLoop状态观察
        CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
        _observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                           kCFRunLoopAllActivities,
                                           YES,
                                           0,
                                           &runLoopObserverCallBack,
                                           &context);
        //将观察者添加到主线程runloop的common模式下的观察中
        CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
        
        // 在子线程监控时长 开启一个持续的loop用来进行监控
        __weak typeof(self) ws = self;
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (YES)
            {
                //假定连续n次超时50ms认为卡顿(当然也包含了单次超时250ms)
                long st = dispatch_semaphore_wait(ws.semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
                if (st != 0)
                {
                    if (!ws.observer)
                    {
                        ws.timeoutCount = 0;
                        ws.semaphore = 0;
                        ws.activity = 0;
                        return;
                    }
                    //两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
                    if (ws.activity==kCFRunLoopBeforeSources || ws.activity==kCFRunLoopAfterWaiting)
                    {
                        if (++ws.timeoutCount < countTimes)
                            continue;
                        NSLog(@"卡啦---------------------------");
                        //打印堆栈信息
                        BSLOG_MAIN
                    }//end activity
                }// end semaphore wait
                ws.timeoutCount = 0;
            }// end while
        });
    }
    
    - (void)startOfflineMonitor {
        [UIViewController displayFPS:YES];
    }
    
    @end
    

    界面启动时间

    一个界面从用户点击到push进来的耗时,是影响用户体验很关键的一点。
    获取控制器加载时间(viewDidLoad -> viewDidAppear):
    通过runtime混写viewDidLoad、viewDidAppear,来记录界面启动时间:

    +(void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [self exchangeInstanceMethod:[self class] method1Sel:@selector(viewDidLoad) method2Sel:@selector(rt_viewDidLoad)];
            [self exchangeInstanceMethod:[self class] method1Sel:@selector(viewDidAppear:) method2Sel:@selector(rt_ld_viewDidAppear:)];
        });
    }
    
    + (void)recordViewLoadTime:(BOOL)yesOrNo {
        recordViewLoadTime = yesOrNo;
    }
    
    + (void)exchangeInstanceMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel {
        
        
        Method originalMethod = class_getInstanceMethod(anClass, method1Sel);
        Method swizzledMethod = class_getInstanceMethod(anClass, method2Sel);
        
        BOOL didAddMethod =
        class_addMethod(anClass,
                        method1Sel,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod(anClass,
                                method2Sel,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        }
        
        else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    }
    
    - (void)rt_viewDidLoad {
        [self rt_viewDidLoad];
        rt_loadingBegin = CACurrentMediaTime();
    
    }
    
    - (void)rt_ld_viewDidAppear:(BOOL)animated {
        [self rt_ld_viewDidAppear:animated];
        if (recordViewLoadTime) {
            [self.class recordViewLoadTime];
        }
    }
    
    + (void)recordViewLoadTime {
        const char *className = class_getName(self.class);
        NSString *classNameStr = @(className);
        
        if ([self needRecordViewLoadTime:classNameStr]) {
            CFTimeInterval end = CACurrentMediaTime();
            NSLog(@"~~~~~~~~~~~%8.2f   ~~~~  className-> %@", (end - rt_loadingBegin) * 1000, classNameStr);
        }
    
    }
    
    + (BOOL)needRecordViewLoadTime:(NSString *)className
    {
        if ([className isEqualToString:@"UIInputWindowController"]) {
            return NO;
        } else if ([className isEqualToString:@"UINavigationController"]) {
            return NO;
        } else {
            return YES;
        }
    }
    

    界面FPS

    iOS界面刷新的频率是每秒60次,如果小于这个值就存在掉帧的情况,掉帧严重会给用户明显的卡顿感觉。
    根据CADisplayLink获取FPS:

    - (void)tick:(CADisplayLink *)link {
        if (_lastTime == 0) {
            _lastTime = link.timestamp;
            return;
        }
        
        _count++;
        NSTimeInterval delta = link.timestamp - _lastTime;
        if (delta < 1) return;
        _lastTime = link.timestamp;
        float fps = _count / delta;
        _count = 0;
        
        CGFloat progress = fps / 60.0;
        UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];
        
        NSString *text1 = [NSString stringWithFormat:@"%d FPS",(int)round(fps)];
        NSLog(@"%@", text1);
    
        
        NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
        [text addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, text.length - 3)];
        [text addAttribute:NSForegroundColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(text.length - 3, 3)];
        [text addAttribute:NSFontAttributeName value:_font range:NSMakeRange(0, text.length)];
        [text addAttribute:NSFontAttributeName value:_subFont range:NSMakeRange(text.length - 4, 1)];
        self.attributedText = text;
    }
    

    GitHub:https://github.com/NK-iOS/DelayedMonitorDemo

    相关文章

      网友评论

        本文标题:iOS:卡顿延迟监测、界面启动耗时、界面FPS

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