前言
离屏渲染,这应该是一个老生常谈的话题了。许多人对于离屏渲染都可以说出一二,而且在面试中,离屏渲染也是一个面试官很爱问的问题,比如说为何会出现离屏渲染,如何防止离屏渲染,可能大家都会说减少圆角,减少边框,减少阴影等等。但是,这是这只是冰山一角。
那么,我们今天就一起来探讨一下离屏渲染。
原理
首先我们看一个例子。
//1.按钮存在背景图片
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 30, 100, 100);
btn1.layer.cornerRadius = 50;
[self.view addSubview:btn1];
btn1.layer.shouldRasterize = YES;
[btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
btn1.layer.masksToBounds = 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"];
这是我们代码中常常会出现的情况。那么,这几种会造成离屏渲染吗?接下来我们通过调试。运行代码。
Simulator -> Debug -> Color Off-screen Rendered
接下来可以看到结果。
通过结果可以看到,第一个和第三个图像出现了离屏渲染,别急,待会儿和大家一一解答。
渲染流程
通过图片可以看到,和正常的渲染流程不同的是,离屏渲染会把数据放入一个离屏缓冲区(Offscreen Buffer)中,待所有图层的结果进行混合计算完成后才会在屏幕进行展示。
举一个例子,如上图所示,在一个相机的图标上添加一个遮罩(Mask),总共会经过这么几步:
- 在App提交到Core Animation
- 再到渲染服务(Render Server),接下来的工作用OpenGL/Metal来进行操作
- 相机按钮来说,首先利用顶点着色器(Vertex Shader)绘制顶点,然后进行图元装配(Primitive Assembly),最后到片元着色器(Pixel Shader)进行渲染,最后获得的数据存入离屏缓冲区(Offscreen Buffer)中。 Pass 1
- 遮罩和 相机的渲染流程类似,渲染完成后存入另一个离屏缓冲区(Offscreen Buffer)中。 Pass 2
- 将Pass 1和Pass 2中存入到离屏缓冲区(Offscreen Buffer)的数据拿出来进行渲染从而展示在界面中。
总结,通过以上的流程我们可以看到,Pass 1和Pass 2两次步骤的数据因为要进行合并和渲染,所以在执行完成后并不能进行丢弃,而必须存入离屏缓冲区这个中间变量,所以就要开辟一个空间,而造成离屏渲染。
用一句通俗的话讲,离屏渲染出现的原因就是App进行额外的渲染和合并,会将中间过程产生的数据存入离屏缓冲区(Offscreen Buffer),从而造成离屏渲染。
离屏渲染的危害
- 离屏渲染会开辟一个离屏缓冲区(Offscreen Buffer),所以会占用额外的存入空间
- 离屏缓冲区(Offscreen Buffer)空间的限制是屏幕像素的2.5倍
- 从离屏缓冲区(Offscreen Buffer)到帧缓冲区(FrameBuffer)这个过程会造成 时间和性能的损耗。
- 容易掉帧,造成性能问题。
离屏渲染触发方式
主动触发 -- 开启光栅化(ShouldRasterize)
When the value of this property is
true
, the layer is rendered as a bitmap in its local coordinate space and then composited to the destination with any other content.
用一句话概括就是当shouldRasterize设成true时,layer被渲染成一个bitmap,并缓存起来,等下次使用时不会再重新去渲染了。
由此可以得知,光栅化开启时,会造成离屏渲染,但是光栅化会对layer进行复用,所以这就是一个很矛盾的点了,离屏渲染会造成性能损耗,但是光栅化又会节约内存,所以到底怎么用光栅化呢?在这里给大家几个建议:
- 如果layer不能被复用,没有必要打开光栅化
- 如果layer不是静态的,会被频繁渲染,开启离屏渲染反而影响效率
- 离屏渲染内容有时间限制,缓存内容100ms以内没有被使用,那么它就会被丢弃,无法复用
- 离屏渲染内容有空间限制,超过屏幕2.5倍像素大小,也会失效,无法复用
被动触发
设置圆角触发的离屏渲染的离屏渲染。
将半径设置为大于0.0的值会使该图层开始在其背景上绘制圆角。默认情况下,拐角半径不适用于图层的contents属性中的图像;它仅适用于图层的背景颜色和边框。但是,将masksToBounds属性设置为true会导致内容被裁剪到圆角。
此属性的默认值为0.0。
通过官方文档,我们可以知道设置cornerRadius仅适用于图层的背景颜色(backgroundColor)和边框(border),而对其中的内容(content)无效,而如果要对内容设置圆角,则需要加上masksToBounds。
clipsToBounds(UIView)是指视图上的子视图,如果超出父视图的部分就截取掉
masksToBounds(CALayer)却是指视图的图层上的子图层,如果超出父图层的部分就截取掉
我们来看一个例子。
UIImageView *img = [[UIImageView alloc]init];
img.frame = CGRectMake(150, 300, 100, 100);
img.backgroundColor = [UIColor blueColor];
[self.view addSubview:img];
img.layer.cornerRadius = 50;
img.image = [UIImage imageNamed:@"btn.png"];
看到结果,
在上面的例子中我们可以看到,虽然设置了img.layer.cornerRadius = 50,但是仍然没有出现圆角:我们对img对象设置了圆角,但是cornerRadius对图片内容无效,所以就导致image里的内容仍然是正方形,盖在img上面后,出现了非圆角的情况。
接下里,我们加上
img.layer.masksToBounds = YES;
可以看到效果
但是,这个时候会触发离屏渲染。因为我们本身背景色是一个图层,而内容又是一个图层,当两个图层叠加而且需要进行组合处理(画圆角)就会触发离屏渲染。
还记得上面那个例子吗?
//1.按钮存在背景图片
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 30, 100, 100);
btn1.layer.cornerRadius = 50;
[self.view addSubview:btn1];
btn1.layer.shouldRasterize = YES;
[btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
btn1.layer.masksToBounds = 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会触发离屏渲染?因为UIButton本身有一个Layer,所以跟Image进行叠加画圆角,会导致离屏渲染。
- 案例2,给UIButton添加背景色,此时只有一个图层,所以不会导致离屏渲染。
- 案例3, UIImageView中本身的Layer和Content的Layer进行叠加,所以导致离屏渲染。
- 案例4,同案例2。
防止
圆角
- 祈求UI小姐姐给切一张图盖在上面,造成圆角的假象,但前提是要和UI小姐姐搞好关系。
- 使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角,需要注意的是Core Graphics通过CPU重新绘制一份带圆角的视图来实现圆角效果,会大大增加CPU的负担,而且相当于多了一份视图拷贝会增加内存开销。但是就显示性能而言,由于没有触发离屏渲染,所以能保持较高帧率。
- 使用CAShapeLayer和UIBezierPath设置圆角,通过设置view.layer的mask属性,可以将另一个layer盖在view上,也可以设置圆角,但是mask同样会触发离屏渲染,但是对内存的消耗最少,而且渲染快速
-(void)pd_setRadius:(float)radius{
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = bounds;
maskLayer.path = maskPath.CGPath;
[self.layer setMask: maskLayer];
}
- CAShapeLayer继承于CALayer,可以使用CALayer的所有属性值;
- CAShapeLayer需要贝塞尔曲线配合使用才有意义(也就是说才有效果)
- 使用CAShapeLayer(属于CoreAnimation)与贝塞尔曲线可以实现不在view的drawRect(继承于CoreGraphics走的是CPU,消耗的性能较大)方法中画出一些想要的图形
- CAShapeLayer动画渲染直接提交到手机的GPU当中,相较于view的drawRect方法使用CPU渲染而言,其效率极高,能大大优化内存使用情况。
- 总的来说就是用CAShapeLayer的内存消耗少,渲染速度快,建议使用优化方案2。
阴影
设置阴影后,设置CALayer的 shadowPath。
let shadowView = UIView()
shadowView.frame = CGRect(x: 50, y: 100, width: 200, height: 200)
shadowView.layer.backgroundColor = UIColor.green.cgColor
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOpacity = 0.5
shadowView.layer.shadowRadius = 10
shadowView.layer.shadowOffset = CGSize(width: 10, height: 10)
shadowView.layer.shadowPath = UIBezierPath(rect: shadowView.bounds).cgPath
view.addSubview(shadowView)