IOS知识积累

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

2019-08-17  本文已影响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

上一篇下一篇

猜你喜欢

热点阅读