iOS实时性能检测

Posted by AndyCao on September 16, 2019

iOS实时性能检测一般有两种方案:

  • FPS检测
  • 主线程卡顿检测

FPS检测

通常情况下,屏幕会保持60hz/s的刷新速度,每次刷新时会发出一个屏幕刷新信号,CADisplayLink允许我们注册一个与刷新信号同步的回调处理。可以通过屏幕刷新机制来展示fps值: CADisplayLink在添加target的时候,会对target产生强引用。为了避免循环引用,先创建一个Proxy。

在Objective-C中,基本上所有的类,最终都继承自NSObject,因此NSObject也被称之为基类。其实还有一个基类,叫做NSProxy。在这里我们创建的WeakProxy就继承于NSProxy。相比于NSObject,NSProxy对象在接收到消息时,会跳过消息传递流程,直接进入消息转发的最后阶段。用在这里,NSProxy响应更加快捷。

@interface WeakProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation WeakProxy
+ (instancetype)proxyWithTarget:(id)target
{
    // NSProxy对象不需要调用init,因为它本来就没有init方法
    WeakProxy *proxy = [WeakProxy alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}

@end

下面是fps监测的核心代码

@interface FPSLabel : UILabel
- (void)startFpsMonitoring;
@end

@interface FPSLabel ()
{
    NSInteger _count;
    CFAbsoluteTime _lastUpadateTime;
}
@property (nonatomic, strong) CADisplayLink *link;
@end

@implementation FPSLabel

- (void)startFpsMonitoring {
    if (self.link) {
        return;
    }
    WeakProxy *proxy = [WeakProxy proxyWithTarget: self];
    self.link = [CADisplayLink displayLinkWithTarget:proxy selector: @selector(displayFps:)];
    [self.link addToRunLoop: [NSRunLoop mainRunLoop] forMode: NSRunLoopCommonModes];
}

- (void)displayFps: (CADisplayLink *)fpsDisplay {
    _count++;
    CFAbsoluteTime threshold = CFAbsoluteTimeGetCurrent() - _lastUpadateTime;
    if (threshold >= 1.0) {
        self.text = [NSString stringWithFormat:@"FPS:%.0f",_count / threshold];
        _lastUpadateTime = CFAbsoluteTimeGetCurrent();
    }
}

@end

可以把FPSLabel添加到任何你想要添加的位置。 经过测试,当fps保持在50以上时,列表的性能还是不错的。当出现卡顿的时候,fps会明显下滑。

通过FPS我们可以很直观的看到app的卡顿情况,但是无法及时采集调用栈信息。通过主线程卡顿监控,可以帮助我们进一步定位到问题所在。

主线程卡顿监控

通过子线程监测主线程的 runLoop,判断两个状态区域之间的耗时是否达到一定阈值。如果对RunLoop有所了解的话,就会知道,NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是说,如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿。

下面是具体代码:

@interface PerformanceMonitor : NSObject
+ (instancetype)sharedInstance;
- (void)start;
- (void)stop;
@end


@interface PerformanceMonitor ()
{
    int _timeoutCount;
    CFRunLoopObserverRef _observer;
    
    @public
    dispatch_semaphore_t _semaphore;
    CFRunLoopActivity _activity;
}
@end

@implementation PerformanceMonitor

+ (instancetype)sharedInstance {
    static PerformanceMonitor *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    PerformanceMonitor *moniotr = (__bridge PerformanceMonitor*)info;
    dispatch_semaphore_t semaphore = moniotr->_semaphore;
    dispatch_semaphore_signal(semaphore);
}

- (void)stop {
    if (!_observer)
        return;
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    CFRelease(_observer);
    _observer = NULL;
}

- (void)start {
    if (_observer) return;
    // 信号
    _semaphore = dispatch_semaphore_create(0);
    // 注册RunLoop状态观察
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    
    // 在子线程监控时长
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES) {
            long st = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
            if (st != 0) {
                if (!_observer) {
                    _timeoutCount = 0;
                    _semaphore = 0;
                    _activity = 0;
                    return;
                }
                
                if (_activity==kCFRunLoopBeforeSources || _activity==kCFRunLoopAfterWaiting){
                    if (++_timeoutCount < 5) continue;
                        
                    // 打印卡顿报告
                    PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
                    PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
                    
                    NSData *data = [crashReporter generateLiveReport];
                    PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
                    NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter withTextFormat:PLCrashReportTextFormatiOS];
                    NSLog(@"------------\n%@\n------------", report);
                }
            }
            _timeoutCount = 0;
        }
    });
}
@end

PLCrashReporter是一个第三方开源库,不仅可以生成崩溃日志,还可以获取堆栈信息。