1、什么是ANR
ANR(Application Not Responding):应用程序无响应,即通常我们说的卡顿,卡顿就是在应用使用过程中出现界面不响应或者界面渲染粘滞的情况。而应用界面的渲染以及事件响应是在主线程完成的,出现卡顿的原因可以归结为主线程阻塞。在开发过程中,遇到的造成主线程阻塞的原因可能是:
- 主线程在进行大量I/O操作:为了方便代码编写,直接在主线程去写入大量数据;
- 主线程在进行大量计算:代码编写不合理,主线程进行复杂计算;
- 大量UI绘制:界面过于复杂,UI绘制需要大量时间;
- 主线程在等锁:主线程需要获得锁A,但是当前某个子线程持有这个锁A,导致主线程不得不等待子线程完成任务。
针对这些问题,如果我们能够捕获得到卡顿当时应用的主线程堆栈,那么问题就迎刃而解了。有了堆栈,就可以知道主线程在什么函数哪一行代码卡住了,是在等什么锁,还是在进行I/O操作,或者是进行复杂计算。有了堆栈,就可以对问题进行针对性解决。
2、卡顿监控方案
参考文章 质量监控-卡顿检测,卡顿监控主要有以下解决方案:
- fps
- ping
- runloop
- stack backtrace
- msgSend observe
2.1 fps
通常情况下,屏幕会保持60hz/s的刷新速度,每次刷新时会发出一个屏幕刷新信号,CADisplayLink允许我们注册一个与刷新信号同步的回调处理。可以通过屏幕刷新机制来展示fps值,具体实现可以参考:软件测试之性能测试(ios)——获取fps(流畅度)
指标 | |
---|---|
卡顿反馈 | 卡顿发生时,fps会有明显下滑。但转场动画等特殊场景也存在下滑情况。高 |
采集精度 | 回调总是需要cpu空闲才能处理,无法及时采集调用栈信息。低 |
性能损耗 | 监听屏幕刷新会频繁唤醒runloop,闲置状态下有一定的损耗。中低 |
实现成本 | 单纯的采用CADisplayLink实现。低 |
结论 | 更适用于开发阶段,线上可作为辅助手段 |
2.2 ping
启动一个监控线程,监控线程每隔一小段时间(delta)ping以下主线程(发送一个dispatch_async任务到主线程),如果主线程此时有空,必然能接收到这个通知,并pong以下,如果监控线程超过delta时间没有收到pong的回复,那么可以推测UI线程必然在处理其他任务了,认为主线程已经发生卡顿,同时打印出当前主线程的函数调用栈。
指标 | |
---|---|
卡顿反馈 | 主线程出现堵塞直到空闲期间都无法回包,但在ping之间的卡顿存在漏查情况。中高 |
采集精度 | 子线程在ping前能获取主线程准确的调用栈信息。中高 |
性能损耗 | 需要常驻线程和采集调用栈。中 |
实现成本 | 需要维护一个常驻线程,以及对象的内存控制。中低 |
结论 | 监控能力、性能损耗和ping频率都成正比,监控效果强 |
2.3 runloop
基于runloop的检测和fps的方案非常相似,都需要依赖于主线程的runloop。由于runloop会调起同步屏幕刷新的callback,如果loop的间隔大于16.67ms,fps自然达不到60hz。而在一个loop当中存在多个阶段,可以监控每一个阶段停留了多长时间
指标 | |
---|---|
卡顿反馈 | runloop的不同阶段把时间分片,如果某个时间片太长,基本认定发生了卡顿。此外应用闲置状态常驻beforeWaiting阶段,此阶段存在误报可能。中 |
采集精度 | 和fps类似的,依附于主线程callback的方案缺少准确采集调用栈的时机,但优于fps检测方案。中低 |
性能损耗 | 此方案不会频繁唤醒runloop,相较于fps性能更佳。低 |
实现成本 | 需要注册runloop observer。中低 |
结论 | 综合性能优于fps,但反馈表现不足,只适合作为辅助工具使用 |
2.4 stack backtrace
代码质量不够好的方法可能会在一段时间内持续占用CPU的资源,换句话说在一段时间内,调用栈总是停留在执行某个地址指令的状态。由于函数调用会发生入栈行为,如果比对两次调用栈的符号信息,前者是后者的符号子集时,可以认为出现了卡顿恶鬼
指标 | |
---|---|
卡顿反馈 | 由于符号地址的唯一性,调用栈比对的准确性高。但需要排除闲置状态下的调用栈信息。高 |
采集精度 | 直接通过调用栈符号信息比对可以准确的获取调用栈信息。高 |
性能损耗 | 需要频繁获取调用栈,需要考虑延后符号化的时机减少损耗。中高 |
实现成本 | 需要维护常驻线程和调用栈追溯算法。中高 |
结论 | 准确率很高的工具,适用面广 |
2.5 msgSend observe
OC方法的调用最终转换成msgSend的调用执行,通过在函数前后插入自定义的函数调用,维护一个函数栈结构可以获取每一个OC方法的调用耗时,以此进行性能分析与优化
指标 | |
---|---|
卡顿反馈 | 高 |
采集精度 | 高 |
性能损耗 | 拦截后调用频次非常高,启动阶段可达10w次以上调用。高 |
实现成本 | 需要维护方法栈和优化拦截算法。高 |
结论 | 准确率很高的工具,但不适用于Swift代码 |
3、基于Ping方案的监控实现
监控代码如下:
@implementation LLPingThread
-(instancetype)initWithThreshold:(double)threshold completion:(LLANRInfoBlock)completion{
self = [super init] ;
if(self){
_isApplicationActive = YES ;
_semaphore = dispatch_semaphore_create(0) ;
_threshold = threshold ;
_completion = completion ;
_isMainThreadANR = NO ;
_stackInfo = @"" ;
_startPingTime = 0.0 ;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil] ;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil] ;
}
return self ;
}
-(void)dealloc{
[[NSNotificationCenter defaultCenter] removeObserver:self] ;
}
-(void)main{
//处理ANR的block
__weak typeof(self) weakSelf = self ;
void (^handleANR)(void) = ^(){
__strong typeof(weakSelf) strongSelf = weakSelf ;
if([strongSelf.stackInfo isEqualToString:@""]){
}else{
if(strongSelf.completion){
double currentTime = floor([[NSDate date] timeIntervalSince1970] * 1000) ;
double duration = (currentTime - strongSelf.startPingTime) / 1000.0 ;
strongSelf.completion(@{
@"stackSymbols":strongSelf.stackInfo,
@"duration":[NSString stringWithFormat:@"%.2f",duration]
}) ;
}
strongSelf.stackInfo = @"" ;
}
};
while(!self.cancelled){
if(_isApplicationActive){
self.isMainThreadANR = YES ;
self.stackInfo = @"" ;
self.startPingTime = floor([[NSDate date] timeIntervalSince1970] * 1000) ;
//如果主线程未阻塞,会执行该代码
dispatch_async(dispatch_get_main_queue(),^{
self.isMainThreadANR = NO ;
dispatch_semaphore_signal(self.semaphore) ;
});
//线程休眠
[NSThread sleepForTimeInterval:self.threshold] ;
//主线程卡顿
if(self.isMainThreadANR){
self.stackInfo = [BSBacktraceLogger bs_backtraceOfMainThread] ;
handleANR() ;
}
//主线程卡顿,等待唤醒
dispatch_wait(self.semaphore, DISPATCH_TIME_FOREVER) ;
}else{
[NSThread sleepForTimeInterval:self.threshold] ;
}
}
}
4、效果演示
测试demo很简单,就是让主线程睡眠5秒钟,代码如下:
-(void)testMainThreadANR{
NSLog(@"5秒钟的卡顿") ;
[NSThread sleepForTimeInterval:5.0] ;
}
该功能已经集成到我开发的SDK里面,效果如下图所示:
4、参考文章
1、https://github.com/Tencent/matrix/wiki/Matrix-for-iOS-macOS-%E5%8D%A1%E9%A1%BF%E7%9B%91%E6%8E%A7%E5%8E%9F%E7%90%86
2、https://github.com/Tencent/matrix/wiki/Matrix-for-iOS-macOS-%E9%85%8D%E7%BD%AE%E8%AF%B4%E6%98%8E
3、https://juejin.im/post/5cacb2baf265da03904bf93b
4、https://github.com/JingJing-Lin/MJRunLoopDemo/blob/master/RunLoopDemo/MJMonitorRunloop.m
5、http://mrpeak.cn/blog/ui-detect/
6、http://hl1987.com/2018/04/27/RunLoop%E6%80%BB%E7%BB%93%EF%BC%9ARunLoop%E7%9A%84%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF%EF%BC%88%E5%9B%9B%EF%BC%89App%E5%8D%A1%E9%A1%BF%E7%9B%91%E6%B5%8B/
7、https://www.jianshu.com/p/ea36e0f2e7ae
8、https://allluckly.cn/%E6%8A%95%E7%A8%BF/tougao73