1、卡顿原理
1.1、界面显现原理
-
CPU:
Layout UI
布局、文本计算、Display
制作、Prepare
图片解码、Commit
提交位图给 GPU -
GPU:用于烘托,将结果放入 FrameBuffer
-
FrameBuffer
:帧缓冲 -
Video Controller:依据
Vsync
(笔直同步)信号,逐行读取 FrameBuffer 中的数据,经过数模转换传递给 Monitor -
Monitor:显现器,用于显现;关于显现模块来说,会按照手机刷新率以固定的频率:1 / 刷新率 向 FrameBuffer 索要数据,这个索要数据的指令便是 笔直同步信号Vsync(低刷60帧为
16.67毫秒
,高刷120帧为 8.33毫秒,下边举例主要以低刷16.67毫秒为主)
1.2、界面撕裂
显现端每16.67ms从 FrameBuffer(帧缓存区)读取一帧数据,假如遇到耗时操作交给不了,那么当时画面就还是旧一帧的画面,但显现过程中,下一帧数据准备结束,导致部分显现的又是新数据,这样就会形成屏幕撕裂
1.3、界面卡顿
- 为了解决界面撕裂,苹果运用
双缓冲机制 + 笔直同步信号
,运用 2个FrameBuffer 存储 GPU 处理结果,显现端替换
从这2个FrameBuffer中读取数据,一个被读取时另一个去缓存;但解决界面撕裂的问题也带来了新的问题:掉帧
- 假如遇到画面带马赛克等状况,导致GPU烘托能力跟不上,会有2种掉帧状况;如图,FrameBuffer2 未烘托完第2帧,下一个16.67ms去 FrameBuffer1 中拿第3帧:
- 掉帧状况1:第3帧烘托结束,接下来需要第4帧,第2帧被丢掉
- 掉帧状况2:第3帧未烘托完,再一个16.67ms去 FrameBuffer2 拿到第2帧,但第1帧多停留了16.67*2毫秒
小结
- 固定的时刻距离会收到
笔直同步信号(Vsync)
,假如 CPU 和 GPU 还没有将下一帧数据放到对应的帧 FrameBuffer缓冲区,就会呈现 掉帧
2、卡顿检测
2.1、CADisplayLink
体系在每次发送 VSync 时,就会触发CADisplayLink
,经过计算每秒发送 VSync 的数量来检查 App 的 FPS 是否稳定
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *link;
@property (nonatomic, assign) NSTimeInterval lastTime; // 每隔1秒记录一次时刻
@property (nonatomic, assign) NSUInteger count; // 记录VSync1秒内发送的数量
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkAction:)];
[_link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)linkAction: (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;
NSLog(@" FPS : %f ", fps);
}
@end
2.2、RunLoop检测
RunLoop 的退出和进入本质都是Observer
的告诉,咱们可以监听Runloop
的状况,并在相关回调里发送信号,假如在设定的时刻内
能够收到信号阐明是流通的;假如在设定的时刻内没有收到信号,阐明发生了卡顿。
#import "LZBlockMonitor.h"
@interface LZBlockMonitor (){
CFRunLoopActivity activity;
}
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;
@end
@implementation LZBlockMonitor
+ (instancetype)sharedInstance {
static id instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, {
instance = [[self alloc] init];
});
return instance;
}
- (void)start{
[self registerObserver];
[self startMonitor];
}
- (void)registerObserver{
CFRunLoopObserverContext context = {0, (__bridge void*)self, NULL, NULL};
//NSIntegerMax : 优先级最小
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
NSIntegerMax,
&CallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
LZBlockMonitor *monitor = (__bridge LZBlockMonitor *)info;
monitor->activity = activity;
// 发送信号
dispatch_semaphore_t semaphore = monitor->_semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)startMonitor{
// 创立信号
_semaphore = dispatch_semaphore_create(0);
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), {
while (YES)
{
// 超时时刻是 1 秒,没有比及信号量,st 就不等于 0, RunLoop 一切的任务
long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
if (st != 0)
{
if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
{
if (++self->_timeoutCount < 2){
NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
continue;
}
// 一秒左右的衡量尺度 很大可能性接连来 防止大规模打印!
NSLog(@"检测到超过两次接连卡顿");
}
}
self->_timeoutCount = 0;
}
});
}
@end
- 主线程监听
kCFRunLoopBeforeSources
(行将处理事情)和kCFRunLoopAfterWaiting
(行将休眠),子线程监控时长,若接连两次 1秒 内没有收到信号,阐明发生了卡顿
2.3、微信matrix
- 微信的
matrix
也是借助 runloop 完成,大体流程与上面 Runloop 办法相同,它运用退火算法
优化捕获卡顿的功率,防止接连捕获相同的卡顿,而且经过保存最近的20
个主线程仓库信息,获取最近最耗时仓库
2.4、滴滴DoraemonKit
-
DoraemonKit
的卡顿检测计划不运用 RunLoop,它也是while
循环中依据一定的状况判别,经过主线程中不断发送信号semaphore
,循环中等候信号的时刻为5秒
,等候超时则阐明主线程卡顿,并进行相关上报
3、优化办法
平时简单的计划有:
- 防止运用 透明UIView
- 尽量运用
PNG
图片 - 防止离屏烘托(圆角运用贝塞尔曲线等)
3.1、预排版
- 便是惯例的在Model层恳求数据后提早将cell高度算好
3.2、预编码 / 解码
-
UIImage 是一个
Model
,二进制流数据 存储在DataBuffer
中,经过decode
解码,加载到imageBuffer
中,最终进入FrameBuffer
才能被烘托 -
当运用 UIImage 或
CGImageSource
的办法创立图片时,图片的数据不会立即解码,而是在设置UIImageView.image
时解码 -
将图片设置到
UIImageView/CALayer.contents
中,然后在CALayer
提交至GPU
烘托前,CGImage
中的数据才进行解码 -
假如任由体系处理,这一步则无法防止,而且会发生在主线程中。假如想防止这个机制,在子线程先将图片制作到
CGBitmapContext
,然后从Bitmap
中创立图片
3.3、按需加载
- 假如方针行与当时行相差超过指定行数,只加载方针滚动规模的前后指定
3行
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{ [needLoadArr removeAllObjects]; } - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{ NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)]; NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject]; NSInteger skipCount = 8; if (labs(cip.row-ip.row)>skipCount) { NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)]; NSMutableArray *arr = [NSMutableArray arrayWithArray:temp]; if (velocity.y<0) { NSIndexPath *indexPath = [temp lastObject]; if (indexPath.row+3<datas.count) { [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]]; [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]]; [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]]; } } else { NSIndexPath *indexPath = [temp firstObject]; if (indexPath.row>3) { [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]]; [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]]; [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]]; } } [needLoadArr addObjectsFromArray:arr]; } }
- 在滑动结束时进行 Cell 的烘托
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView{ scrollToToping = YES; return YES; } - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{ scrollToToping = NO; [self loadContent]; } - (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView{ scrollToToping = NO; [self loadContent]; } //用户接触时第一时刻加载内容 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ if (!scrollToToping) { [needLoadArr removeAllObjects]; [self loadContent]; } return [super hitTest:point withEvent:event]; } - (void)loadContent{ if (scrollToToping) { return; } if (self.indexPathsForVisibleRows.count<=0) { return; } if (self.visibleCells && self.visibleCells.count>0) { for (id temp in [self.visibleCells copy]) { VVeboTableViewCell *cell = (VVeboTableViewCell *)temp; [cell draw]; } } }
- 这种办法会导致滑动时有空白内容,因而要做好占位内容
3.4、异步烘托
- 异步烘托 便是在子线程把需要制作的图形提早处理好,然后将处理好的图像数据直接返给主线程运用
- 异步烘托操作的是
layer
层,将多层堆叠的控件们经过UIGraphics
画成一张位图
,然后展现在layer.content
上
3.4.1、CALayer
-
CALayer
根据CoreAnimation
从而根据QuartzCode
,只负责显现,且显现的是位图
,不能处理用户的接触事情 - 不需要与用户交互时,运用 UIView 和 CALayer 都可以,乃至 CALayer 更简练高效
3.4.2、异步烘托完成
- 异步烘托的结构引荐:
Graver
、YYAsyncLayer
-
CALayer 在调用
display
办法后回去调用制作相关的办法,制作会履行drawRect:
办法
简单比如
-
承继 CALayer
#import "LZLayer.h" @implementation LZLayer //前面断点调用写下的代码 - (void)layoutSublayers{ if (self.delegate && [self.delegate respondsToSelector:@selector(layoutSublayersOfLayer:)]) { //UIView [self.delegate layoutSublayersOfLayer:self]; }else{ [super layoutSublayers]; } } //制作流程的发起函数 - (void)display{ // Graver 完成思路 CGContextRef context = (__bridge CGContextRef)([self.delegate performSelector:@selector(createContext)]); [self.delegate layerWillDraw:self]; [self drawInContext:context]; [self.delegate displayLayer:self]; [self.delegate performSelector:@selector(closeContext)]; } @end
-
承继 UIView
// - (CGContextRef)createContext 和 - (void)closeContext要在.h中声明 #import "LZView.h" #import "LZLayer.h" @implementation LZView - (void)drawRect:(CGRect)rect { // Drawing code, 制作的操作, BackingStore(额外的存储区域产于的) -- GPU } //子视图的布局 - (void)layoutSubviews{ [super layoutSubviews]; } + (Class)layerClass{ return [LZLayer class]; } // - (void)layoutSublayersOfLayer:(CALayer *)layer{ [super layoutSublayersOfLayer:layer]; [self layoutSubviews]; } - (CGContextRef)createContext{ UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.layer.opaque, self.layer.contentsScale); CGContextRef context = UIGraphicsGetCurrentContext(); return context; } - (void)layerWillDraw:(CALayer *)layer{ //制作的准备工作,do nontihing } //制作的操作 - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{ [super drawLayer:layer inContext:ctx]; // 画个不规则图形 CGContextMoveToPoint(ctx, self.bounds.size.width / 2- 20, 20); CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 + 20, 20); CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 + 40, 80); CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 - 40, 100); CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 - 20, 20); CGContextSetFillColorWithColor(ctx, UIColor.magentaColor.CGColor); CGContextSetStrokeColorWithColor(ctx, UIColor.magentaColor.CGColor); // 描边 CGContextDrawPath(ctx, kCGPathFillStroke); // 画个赤色方块 [[UIColor redColor] set]; //Core Graphics UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(self.bounds.size.width / 2- 20, self.bounds.size.height / 2- 20, 40, 40)]; CGContextAddPath(ctx, path.CGPath); CGContextFillPath(ctx); // 文字 [@"LZ" drawInRect:CGRectMake(self.bounds.size.width / 2 - 40, 100, 80, 24) withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:20],NSForegroundColorAttributeName: UIColor.blueColor}]; // 图片 [[UIImage imageWithContentsOfFile:@"/Volumes/Disk_D/test code/Test/Test/yasuo.png"] drawInRect:CGRectMake(10, self.bounds.size.height/2, self.bounds.size.width - 20, self.bounds.size.height/2 -10)]; } //layer.contents = (位图) - (void)displayLayer:(CALayer *)layer{ UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); dispatch_async(dispatch_get_main_queue(), { layer.contents = (__bridge id)(image.CGImage); }); } - (void)closeContext{ UIGraphicsEndImageContext(); }
-
控件们被制作成了一张图
-
此外,虽然将控件画到一张位图上,可是还有问题,便是控件的交互事情,内容较多建议研究一下graver的源码