卡顿原理
CPU耗时,产生丢帧
一帧CPU和 GPU加载流程
CPU计算, GPU渲染
屏幕显示采取双缓冲区
当帧缓冲区2 比较耗时没有完成,切换读取帧缓冲区1完成后,帧缓冲区2 仍然没有完成,再次读取帧缓冲区1,造成帧缓冲区2 不显示,产生丢帧
卡顿检测
1.CADisplayLink
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
1秒调用多少次即fps
1秒 if (delta < 1) return;
- (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;
}
2.NSRunLoop检测
通过监听NSRunLoop的生命周期,查看在规定时间内,NSRunLoop任务执行情况
通过信号量等待时间完成 规定时间1秒
dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
监听NSRunLoop的生命周期
static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
LGBlockMonitor *monitor = (__bridge LGBlockMonitor *)info;
NSLog(@"%lu", activity);
monitor->activity = activity;
// 发送信号
dispatch_semaphore_t semaphore = monitor->_semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//NSIntegerMax : 优先级最小
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
NSIntegerMax,
&CallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
界面优化
1. 预排版
加载数据时完成,cell 高度的计算
2.预解码
对图片解码后 数据的处理
SDWebImage对图片解码的处理
// decode the image in coder queue
dispatch_async(self.coderQueue, ^{
@autoreleasepool {
UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
CGSize imageSize = image.size;
if (imageSize.width == 0 || imageSize.height == 0) {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
}else {
[self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
}
[self done];
}
});
图片避免无谓的解码操作
DownSampling 就是在Decode的时候指定尺寸,只Decode部分数据,减少内存的使用
比如我一个控件大小是100 * 100,但是原图可能是300 * 300的。使用DownSampling后,只需要解码少量数据就可以达到所需。
这个SDWebImage也已经支持,大家只需在加载图片的时候,利用context参数设置图片的大小和控件的大小相同即可。
@{SDWebImageContextImageThumbnailPixelSize:@(size)}
swift 图片加载框架Kingfisher
图片复用
对于使用频繁的图片,可以使用[UIImage imageWithNamed:@""]方式创建,利用系统级别的缓存来提高效率,减少内存。
对于使用不频繁的图片,建议使用直接读文件的方式加载,用完就会自动释放,减少内存。
cell 将要展示,加载图片
override func collectionView(
_ collectionView: UICollectionView,
willDisplay cell:UICollectionViewCell,
forItemAt indexPath:IndexPath)
{
let imageView = (cell as! ImageCollectionViewCell).cellImageView!
let url = ImageLoader.sampleImageURLs[indexPath.row % ImageLoader.sampleImageURLs.count]
KF.url(url, cacheKey:"\(url.absoluteString)-\(indexPath.row)")
.downsampling(size:CGSize(width: 250, height: 250))
.cacheOriginalImage()
.set(to: imageView)
}
cell 将要消失时,取消加载图片
override func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath)
{
(cellas! ImageCollectionViewCell).cellImageView.kf.cancelDownloadTask()
}
3.离屏渲染
离屏渲染检测
1、模拟器下检测:Simulator --> Debug --> Color Off-screen rendered,模拟器下只需要设置模拟器一次就可以
2、真机下检测:XCode --> Debug --> View Debugging -->Rendering --> Color Offscreen-Rendered Yellow,真机下每次运行后都要在XCode中设置一下生效
离屏渲染产生原因
正常情况下,系统会按照60FPS或者120FPS的频率来执行渲染流程。在每个屏幕渲染周期内,系统会从帧的缓冲区里拿到已经渲染好的数据,渲染到屏幕上。而由于图层或者其他因素,导致在屏幕内无法直接渲染,需要在屏幕外开辟一个空间用来合成帧数据。这就是所谓的离屏渲染
离屏渲染的坏处
1.开辟了一块额外的空间,内存增加了
2.切换环境造成的牺牲很大
很容易发生在渲染周期内,数据无法渲染好,因此造成卡顿问题。
造成离屏渲染的方式
关于离屏渲染,实际开发中基本上都是:
圆角+剪裁的组合,并不是所有圆角+裁剪都会产生离屏渲染
1. 一旦设置圆角+裁剪,如果视图一定是有contents(图片、绘制内容、有图像信息的子视图),加上背景色或者边框,就会产生离屏渲染。
2.设置圆角+裁剪,加上子视图位于裁剪区域,也会离屏渲染。
3. 仅有圆角+裁剪,和contents是不会离屏渲染的。经典例子就是【button setImage】的了,只需要对button.imageView.layer.cornerRadius和button.imageView.clipsToBounds进行就不会离屏渲染
设置layer的mask
设置阴影
光栅化,shouldRasterize,一旦设置为true,就会把layer的渲染结果包括其子layer,以及圆角、阴影、group opacity等等)保存在一块内存中,这样一来在下一帧仍然可以被复用,而不会再次触发离屏渲染。主旨在于降低性能损失,但总是至少会触发一次离屏渲染。
抗锯齿
解决离屏渲染
对于设置阴影造成的离屏渲染,解决方式就是使用贝塞尔曲线绘制好path,这样就能解决问题
对于UIImageView的圆角方案
让设计师切一个遮罩盖在上面是最好的解决方案
苹果认为组合subViews的方式比自己绘制的方式好很多
离屏渲染参考连接
逻辑教育公众号
4.界面按需加载
使用数组储存当前页面的cell + 3 个, cell加载时 根据数组进行判断
//按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行加载。
- (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) {
[arraddObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];
[arraddObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];
[arraddObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];
}
}else {
NSIndexPath *indexPath = [temp firstObject];
if (indexPath.row>3) {
[arraddObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
[arraddObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
[arraddObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
}
}
[needLoadArr addObjectsFromArray:arr];
}
}
5.异步渲染
框架美团Graver
将页面内所有组件 构造成一张图片,放到layer进行加载
1.重写layer
+ (Class)layerClass{
NSLog(@"layerClass");
return [LGLayer class];
}
2.layer 布局和展示
layer 布局
- (void)layoutSublayers{
if (self.delegate && [self.delegate respondsToSelector:@selector(layoutSublayersOfLayer:)]) {
//UIView
[self.delegate layoutSublayersOfLayer:self];
}else{
[super layoutSublayers];
}
}
layer展示
绘制流程的发起函数
- (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)];
}
3. UIView实现
layer布局
- (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
NSLog(@"layerWillDraw");
}
绘制的操作
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
[super drawLayer:layer inContext:ctx];
NSLog(@"drawLayer");
[[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);
}
加载位图 layer.contents = (位图)
- (void)displayLayer:(CALayer *)layer{
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
layer.contents = (__bridge id)(image.CGImage);
});
}
- (void)closeContext{
UIGraphicsEndImageContext();
}