离屏渲染问题是在面试中经常会被问到的一个问题,都知道设置layer圆角,会触发离屏渲染,那么只有设置圆角就会触发离屏渲染吗?
那么,我们来写一段代码测试一下,是否是所有的圆角都会触发
离屏渲染。
我们定义几个不同情况的按钮,设置圆角,测试代码如下:
//1.按钮存在背景图片
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 80, 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, 200, 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, 340, 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, 520, 100, 100);
[self.view addSubview:img2];
img2.layer.cornerRadius = 50;
img2.layer.masksToBounds = YES;
img2.image = [UIImage imageNamed:@"btn.png"];

当开启离屏渲染时,如上对比图,发现并不是所有的圆角都触发了离屏渲染。
接下来分析一下离屏渲染触发的原因。
离屏渲染触发的原因
屏幕显示数据有两种加载流程:
- 正常渲染加载流程
- 离屏渲染加载流程
这两种加载流程简单示意图如下:

接下来仔细说一下这两种加载流程
-
正常渲染流程
正常的
APP加载是数据经过CPU和GPU的计算后,存储在帧缓冲区 Frame Buffer中,屏幕不断从帧缓冲区读取数据显示到屏幕上的。在
GPU渲染流程中,显示到屏幕上的图像遵循画家算法,按照从远到近的原则,依次将结果存储到帧缓冲区,视频控制器从Frame Buffer中,读取一帧数据后,显示到屏幕上,然后立即丢弃这一帧数据,不做任何保留,这样做的目的是节省空间,且在屏幕上各自显示,互不影响。 -
离屏渲染流程
当需要渲染特殊效果时(比如:圆角、毛玻璃、高斯模糊、阴影等),不能一次性得到渲染结果,这时,就需要将渲染处理好的数据不直接放到
Frame Buffer中,而是放到离屏缓冲区 offScreen Buffer中,最后把几个图层的结果叠加之后,放到Frame buffer中,然后显示到屏幕上。
离屏缓冲区 offScreen Buffer是额外开辟的一个缓冲区,也是有空间限制,其大小为屏幕像素点的2.5倍,其开销也是很大的,那为什么还要用离屏渲染呢?
- 特殊效果,需要使用
offscreen buffer保存中间状态,不得不使用离屏渲染,是系统自动触发,比如: 遮罩,圆角,高斯模糊,阴影,毛玻璃效果 - 效率优势,既然效果要多次出现在屏幕上,可以提前渲染,保存到
offscreen buffer中,达到复用的目的。是手动触发,以达到复用目的、 抗锯齿目的。
接下来以遮罩为例,分析一下,为什么需要离屏渲染以及渲染的流程。

如上图,想要实现图片③的效果,需要把图片②覆盖在图片①上,最终渲染成图③的样子,用正常的渲染流程,从帧缓冲区读取一帧数据,显示,然后丢弃,无法到达这种遮罩的效果,所以需要将渲染好的图层,存放到离屏缓冲区 offscreen buffer中,等全部图层都处理完毕后,叠加混合,存到帧缓冲区中,然后视频控制器读取帧缓冲区中数据,显示到屏幕上。
详细流程如下:

毛玻璃效果的流程和遮罩类型,不在详细描述,流程如下:

触发离屏渲染的另一个原因--光栅化 shouldRasterize
When the value of this property is YES, the layer is rendered as a bitmap in its local coordinate space and then composited to the destination with any other content.
当我开启光栅化,设置shouldRasterize为YES时,会将layer渲染成的位图保存在offscreen buffer中,下次直接使用,提供效率。
光栅化使用建议:
- 如果
layer不能复用,则没有必要打开光栅化 - 如果
layer不是静态的,需要频繁被渲染,比如存在于动画中,那么开了光栅化,离屏渲染反而影响了效率 - 离屏渲染缓存内容有时间限制,缓存内容
100ms内,没有被使用,那么会被丢弃,无法再使用 - 离屏渲染缓存空间有限,超过屏幕像素的2.5倍,就会失效,且无法被复用
圆角中的离屏渲染
首先看一下CALayer的构成,如下图:是有backgroundColor、contents、borderWidth borderColor构成,设置圆角,为什么会触发离屏渲染的原因,就在这里。

在设置圆角的时候,我们都知道,在设置完cornerRadius、borderWidth、borderColor等属性后,我们还要设置masksToBounds为YES才会生效,起原理是大部人都没有去仔细研究的。
下面是苹果官方文档针对圆角设置的一些说明:

从文档中得知:在设置了cornerRadius,只会对backgroundColor 和 boder设置圆角,不会设置contents的圆角,除非同时设置了maskToBounds / clipsToBounds属性。
也可以理解为:圆角不生效的根本原因是没有对contents设置圆角,而设置的image是放在contents里面的,所以看到的界面上的就是image没有进行圆角裁剪。这也验证了开篇的那个一段测试代码。
小结:
- 当只设置
backgroundColor、border,而contents中没有子视图时(可以简单的理解为没有设置图片),无论maskToBounds / clipsToBounds是true还是false,都不会触发离屏渲染 - 当
contents中有子视图时,此时设置cornerRadius + maskToBounds / clipsToBounds,就会触发离屏渲染,但是这种情况在UIImageView中并不适用 - 当
UIImageView中只设置图片和maskToBounds / clipsToBounds是不会触发离屏渲染(开篇有例子),苹果对UIImageView优化猜测是只是将image直接画在了contents上面,这样不设置背景色其实只需要渲染一个layer,所以不需要用到离屏缓冲区,也就不会触发离屏渲染,如果此时再加上背景色,就会触发离屏渲染。
常见触发离屏渲染的几种情况:
- 使用了
mask的layer(layer.mask) - 需要进行裁剪的
layer,maskToBounds / clipsToBounds - 设置了透明度为
yes,并且透明度不为1 - 添加了投影的
layer,(layer.shadow) - 采用了光栅的
layer,(layer.shouldRasterize) - 绘制了文字的
layer,(UILabel, CATextLayer, Core Text)