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是一个第三方开源库,不仅可以生成崩溃日志,还可以获取堆栈信息。