离屏渲染可谓是iOS开发中老生常谈的问题了,不管是开发中尽量避免也好,还是面试中这是最高频的面试题目也好。大多数人都知道,设置View
的阴影圆角等容易触发离屏渲染,大多数人都知道这个概念,也能说出个大概,但是了解表面现象并不是我们本意,我们需要探索触发离屏渲染的本质,真正的揭开离屏渲染的神秘面纱。
在OpenGL中,渲染缓冲区有两种:
On-Screen Rendering (当前屏幕渲染)
指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区进行。APP
在显示的时候,是通过GPU
和CPU
合作,不断的从FrameBuffer渲染到屏幕上。
Off-Screen Rendering (离屏渲染)
指的是在GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作。因为有些图形是需要进行混合计算,保留一个中间形态,然后再显示在屏幕上的。
离屏渲染出发原因如下图所示:①为遮罩层,②为背景图,③为合成后的图片。
首先要显示图片③,那么在计算机底层通过顶点着色器和片源着色器开始绘制的时候,是不可能直接渲染完成的,计算机也要经过运算,首先要在内部渲染①和②,此时的①和②的遮罩纹理和图层纹理不能直接显示在屏幕上,所以需要一个中间态来保存,也就是离屏渲染缓冲区,然后再第三步的时候,由帧缓冲区提交显示。需要注意,离屏渲染空间大小有限制,为当前屏幕的2.5倍,超过这个限制,就不能存放在离屏缓冲区。

结合上图,我们知道离屏渲染的代价很高,想要进行离屏渲染,首选要创建一个新的缓冲区,屏幕渲染会有一个上下文环境,离屏渲染的整个过程需要切换上下文环境,先从当前屏幕切换到离屏,等结束后,又要将上下文环境切换回来。这也是为什么会消耗性能的原因了。那么我们就会有疑问,既然离屏渲染会有性能问题,为什么还会需要离屏渲染?
离屏渲染存在的作用:
1、有些特殊效果不能一次性的直接显示出来,需要使用额外的
Off-Screen Buffer
来保存中间状态,这些是由系统自动触发的。
2、离屏渲染可以带来效率的提升,因为某些效果会多次出现在屏幕的时候,需要提前渲染到Off-Screen Buffer
,来达到复用的目的,这些是手动触发的。
离屏渲染触发的第二个原因:ShouldRasterize光栅化
当一个图像混合了多个图层,每次移动时,每一帧都要重新合成这些图层,十分消耗性能。当我们开启光栅化后,会在首次产生一个位图缓存,当再次使用时候就会复用这个缓存。但是如果图层发生改变的时候就会重新产生位图缓存。
光栅化使用建议:
1.如果layer
不能被复用,则没有必要打开光栅化;
2.如果layer
不是静态的,需要被频繁修改,比如处于动画之中,那么开启离屏渲染反而影响效率;
3.离屏渲染缓存内容有时间限制,缓存内容100ms内容如果没有被使用,那么就会丢弃,无法进行复用;
4.离屏渲染缓存空间有限,超过2.5倍屏幕像素大小的话,也会失效,且无法进行复用;
需要注意并不是所有的设置layer
的圆角就会触发离屏渲染,要出发离屏渲染有很多的条件限制,下图可以很好地解释怎么样触发,接下来我也会用案例来说明。

一个图片包含边框,内容,背景,设置
layer.cornerRadius
指挥设置背景和border
圆角,不会设置content
的圆角,除非同时设置了layer.masksToBounds
为YES
(对应View
中的ClipsToBounds
)。
//1.按钮存在背景图片
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 30, 100, 100);
btn1.layer.cornerRadius = 50;
[self.view addSubview:btn1];
[btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
btn1.clipsToBounds = YES;
//2.按钮不存在背景图片
UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
btn2.frame = CGRectMake(100, 180, 100, 100);
btn2.layer.cornerRadius = 50;
btn2.backgroundColor = [UIColor blueColor];
[self.view addSubview:btn2];
btn2.clipsToBounds = YES;
//3.UIImageView 设置了图片+背景色;
UIImageView *img1 = [[UIImageView alloc]init];
img1.frame = CGRectMake(100, 320, 100, 100);
img1.backgroundColor = [UIColor blueColor];
[self.view addSubview:img1];
img1.layer.cornerRadius = 50;
img1.layer.masksToBounds = YES;
img1.image = [UIImage imageNamed:@"btn.png"];
//4.UIImageView 只设置了图片,无背景色;
UIImageView *img2 = [[UIImageView alloc]init];
img2.frame = CGRectMake(100, 480, 100, 100);
[self.view addSubview:img2];
img2.layer.cornerRadius = 50;
img2.layer.masksToBounds = YES;
img2.image = [UIImage imageNamed:@"btn.png"];
上面案例中,1,3会出发离屏渲染,2,4不会,因为2,4只对View
的单一属性进行了修改,不需要额外的离屏缓冲区来保存纹理数据,所以不会出发离屏渲染。下面是我总结的离屏渲染常见的触发情况:
1.使⽤了
mask
的layer(layer.mask)
2.需要进⾏裁剪的layer (layer.masksToBounds / view.clipsToBounds)
3.设置了组透明度为YES
,并且透明度不为 1 的layer (layer.allowsGroupOpacity/layer.opacity)
4.添加了投影的layer (layer.shadow*)
5.采⽤了光栅化的layer (layer.shouldRasterize)
6.绘制了⽂字的layer (UILabel, CATextLayer, Core Text 等)
在实际的项目中,我们在处理圆角方案的时候,可以有很多种来避免离屏渲染,最常见的方案有:
方案一
iOS 9.0
之后给有背景的UIButton
设置圆角会触发离屏渲染,而UIImageView
里PNG
图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。
imageView.layer.cornerRadius = CGFloat(10);
imageView.layer.masksToBounds = YES;
方案二
使用贝塞尔曲线UIBezierPath
和Core Graphics
框架画出一个圆角。
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
//开始对imageView进行画图
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
//使用贝塞尔曲线画出一个圆形图
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds
cornerRadius:imageView.frame.size.width] addClip];
[imageView drawRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
//结束画图
UIGraphicsEndImageContext();
[self.view addSubview:imageView];
方案三
使用CAShapeLayer
和UIBezierPath
设置圆角。
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds
byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
//设置大小
maskLayer.frame = imageView.bounds;
//设置图形样子
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer;
[self.view addSubview:imageView];
方案三比方案二消耗内存更小,渲染速度更快,建议使用方案三。
Shadow
优化
对于shadow
,如果图层是个简单的几何图形或者圆角图形,我们可以通过设置shadowPath
来优化性能,能大幅提高性能。示例如下:
imageView.layer.shadowColor = [UIColor grayColor].CGColor;
imageView.layer.shadowOpacity = 1.0;
imageView.layer.shadowRadius = 2.0;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];
imageView.layer.shadowPath = path.CGPath;
最后是一些其他的优化建议:
1.当我们需要圆角效果时,可以让UI将图切好,这是最简单直接的方案
2.减少图片层级,因为图片绘制复杂度以层级的指数倍增加
3.尽量使用不包含透明alpha
通道的图片资源
4.使用代码手动生成圆角Image
设置到要显示的View
上,利用UIBezierPath
画出来圆角
网友评论