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;
}