参考iOS 性能优化总结
参考微信读书 iOS 性能优化总结
参考iOS实时卡顿监控
参考移动端IM实践:iOS版微信界面卡顿监测方案
参考iOS保持界面流畅的技巧
本文是对上面几篇文章的总结。并与我自己的知识体系相融合,以达到一个自相融洽的整体。
卡顿原理
VSync信号到来时,系统图形服务会通过CADisplayLink等机制通知App,APP主线程开始在CPU中计算显示内容,比如视图的创建,布局计算,图片解码,文本绘制等。随后CPU会将计算好的内容提交到GPU,由GPU进行变换,合成,渲染。随后GPU会把渲染结果提交到帧缓冲区区,等待下一次VSync信号到来时显示到屏幕上。
由于垂直同步机制,若果在一个VSync时间内,CPU或者GPU没有完成内容的提交,则这个没有完成的提交不会显示到屏幕,在其提交后会到缓冲区,等待下一次机会在显示。那么这个下一次有可能是正常时间提交修改,则会将缓存区的内容覆盖。导致被覆盖的永远没机会显示了。就是掉帧了。
那么与runloop有什么关系呢?
Runloop的时间概念比1/60更小,也就是Runloop处理事物的时间远远比1/60要小。绝大部分时间是睡眠的。所以它们两个本身是没有关系的。绝大部分时候是主线程早就处理完毕所有显示数据,并提交到了渲染系统,渲染系统也完成了合成,也就是提交到了缓存区。只需要等待时钟的到来,将这个缓冲区的内容显示到屏幕上。
FPS的真正含义是,1s的时间真正有多少帧显示到了屏幕上。
卡顿监控思路
主线程卡顿监控
通过子线程监测主线程的runloop,判断两个状态区域之间的耗时是否达到一定阈值。这两个状态就是kCFRunLoopBeforeSources与kCFRunLoopAfterWaiting借助FPS监控
CADisplayLink 可以将其看做一个定时器。定时器都与机器的硬件有关系。而这个定时器是有屏幕“垂直时钟”驱动。也就是与垂直时钟同步,1/60时间跳动一次。因为CADisplayLink的回调也要在线程里执行,将其加入到主线程的Runloop里。runloop的TimerSource就会触发runloop的“叫醒”,执行该执行的内容。若果不将CADisplayLink加入,当然就不会定固定时间“叫醒”。
CADislayLink的timestampe的两次差为1s之内的tick次数,即使FPS。
线程卡顿监控方案一的实现
iOS实时卡顿检测
这一方案的思路,就是从引起卡顿的本质来优化。引起卡顿,在kCFRunLoopBeforeSources通知后执行主队列里的代码,执行block的代码等。或则在kCFRunLoopAfterWaiting被唤起后,也会执行队列里的代码,block代码。
#import <CrashReporter/CrashReporter.h>
@interface PerformanceMonitor ()
{
int timeoutCount;
CFRunLoopObserverRef observer;
@public
dispatch_semaphore_t semaphore;
CFRunLoopActivity activity;
}
@end
@implementation PerformanceMonitor
+ (instancetype)sharedInstance
{
static id 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;
moniotr->activity = activity;
dispatch_semaphore_t semaphore = moniotr->semaphore;
dispatch_semaphore_signal(semaphore);//没当状态发生变化的时候+1;这是在主线程执行
}
- (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);
//在子线程监控时长
//在子线程不断的监控信号量,如果在50 ms里还没有超时,也就是在50ms状态没有发生该边的话。就说明在两个状态的时间间隔里有执行时长超过50ms的
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;
}
//若果超时的是kCFRunLoopBeforeSources 或者kCFRunLoopAfterWaiting 就更能说明问题
//如果5此都是这样,说明有问题。几下主线程的调用堆栈。
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