一、了解离屏渲染
1、正常渲染流程
APP -----> FrameBuffer(帧缓冲区) -----> Display
- APP中的数据经过CPU和GPU计算渲染后,把结果放入帧缓冲区,再由视频控制器从甄嬛从去中读取并显示
- GPU渲染过程中,显示到屏幕上的图像会遵循“画家算法”由远到近的顺序,依次将结果存储到帧缓冲区
-
视频控制器从帧缓冲区读取一帧的数据将其显示后,就立刻丢弃了这一帧数据,然后进行下一帧的渲染显示。这样做的好处是节省了空间。
2、离屏渲染流程及具体逻辑
APP -----> OffScreenBuffer(离屏缓冲区) -----> FrameBuffer(帧缓冲区) -----> Display
- 当APP要进行额外的渲染和合并时(比如设置了圆角+裁剪),我们需要把不同的图层进行裁剪+合并的操作,这时就不能直接放入FrameBuffer了,我们要把渲染好的结果放入OffScreenBuffer,等待合适的机会将几个图层进行裁剪、合并叠加的操作,完成后把结果放入FrameBuffer中,由视频控制器读取显示
- 离屏缓冲区相当于一个临时缓冲区,存放需要进行操作的数据,并不直接使用数据。因此,在方便我们的同时也有缺点,因为是额外开辟的空间,并且还需要转存数据到FrameBuffer中,所以大量的离屏渲染会影响性能,开销较大,也可能造成掉帧
-
OffScreenBuffer空间也是有限制的,是屏幕像素的2.5倍。如果缓存内容并100ms未被使用,会直接丢弃。
二、离屏渲染触发的条件
我们通过代码调试来验证一下,通过打开模拟器的离屏选项来观察
1、高斯模糊 UIBlurEffectView(必定触发)
//Button 背景色
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 50, 100, 100);
btn1.backgroundColor = [UIColor redColor];
[self.view addSubview:btn1];
//Button 背景色+高斯模糊
UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
btn2.frame = CGRectMake(100, CGRectGetMaxY(btn1.frame)+50, 100, 100);
btn2.backgroundColor = [UIColor redColor];
[self.view addSubview:btn2];
UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
UIVisualEffectView *effectVIew = [[UIVisualEffectView alloc]initWithEffect:effect];
effectVIew.frame = btn2.bounds;
[btn2 addSubview:effectVIew];
那么我们来看一下高斯模糊的离屏渲染逻辑
- Content : 渲染内容
- Capture Content : 捕获内容
- Horizontal Blur : 水平模糊
- Vertical Blur :垂直模糊
- Compositing Pass : 合成过程
2、光栅化(必定触发)
//Button 背景色
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 50, 100, 100);
btn1.backgroundColor = [UIColor redColor];
[self.view addSubview:btn1];
//Button 背景色+光栅化
UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
btn2.frame = CGRectMake(100, CGRectGetMaxY(btn1.frame)+50, 100, 100);
btn2.backgroundColor = [UIColor redColor];
btn2.layer.shouldRasterize = YES;
[self.view addSubview:btn2];
开启光栅化后,会触发离屏渲染,Render Server 会强制将 CALayer 的渲染位图结果 bitmap 保存下来,这样下次再需要渲染时就可以直接复用,从而提高效率。
而保存的 bitmap 包含 layer 的 subLayer、圆角、阴影、组透明度 group opacity 等,所以如果 layer 的构成包含上述几种元素,结构复杂且需要反复利用,那么就可以考虑打开光栅化。
使用光栅化shouldRasterize的一些建议:
1、如果layer不能被复用,没必要打开光栅化
2、如果layer是动态的,需要频繁修改,打开光栅化会造成很大的负荷,不建议打开
3、离屏缓冲区内容有时间限制,超过100ms没有被使用会被丢弃,无法复用
4、离屏缓冲区空间大小有限制,超过屏幕2.5倍就会失效,无法复用
3、阴影
//Button 背景色
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 50, 100, 100);
btn1.backgroundColor = [UIColor redColor];
[self.view addSubview:btn1];
//Button 背景色+阴影
UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
btn2.frame = CGRectMake(100, CGRectGetMaxY(btn1.frame)+50, 100, 100);
btn2.backgroundColor = [UIColor redColor];
[self.view addSubview:btn2];
btn2.layer.shadowColor = UIColor.blackColor.CGColor;
btn2.layer.shadowOffset = CGSizeMake(2, 2);
btn2.layer.shadowOpacity = 0.9;
不过,阴影存在优化方案,就是指定一下阴影路径,就能解决了
//在上述代码的基础上添加
btn2.layer.shadowPath = [UIBezierPath bezierPathWithRect:btn2.bounds].CGPath;
4、圆角
我们先以UIButton和UIImageView为例,看不同条件下,圆角是否触发离屏渲染
//针对UIButton的圆角分情况测试
for (int i = 0; i < 5; i++) {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.frame = CGRectMake(50, 150*i + 50, 100, 100);
btn.layer.cornerRadius = 50;
btn.clipsToBounds = YES;
[self.view addSubview:btn];
if (i == 0) {
//背景色+边框+图片
btn.backgroundColor = [UIColor redColor];
btn.layer.borderWidth = 2;
[btn setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
}else if (i == 1){
//背景色+边框
btn.backgroundColor = [UIColor redColor];
btn.layer.borderWidth = 2;
}else if (i == 2){
//背景色+图片
btn.backgroundColor = [UIColor redColor];
[btn setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
}else if (i == 3){
//边框+图片
btn.layer.borderWidth = 2;
[btn setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
}else if (i == 4){
//图片
[btn setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
}
}
//针对UIImageView的圆角分情况测试
for (int j = 0; j < 5; j++) {
UIImageView *img = [[UIImageView alloc]init];
img.frame = CGRectMake(200, 150*j + 50, 100, 100);
img.layer.cornerRadius = 50;
img.layer.masksToBounds = YES;
[self.view addSubview:img];
if (j == 0) {
//背景色+边框+图片
img.backgroundColor = [UIColor redColor];
img.layer.borderWidth = 2;
img.image = [UIImage imageNamed:@"btn.png"];
}else if (j == 1){
//背景色+边框
img.layer.borderWidth = 2;
img.backgroundColor = [UIColor redColor];
}else if (j == 2){
//背景色+图片
img.backgroundColor = [UIColor redColor];
img.image = [UIImage imageNamed:@"btn.png"];
}else if (j == 3){
//边框+图片
img.layer.borderWidth = 2;
img.image = [UIImage imageNamed:@"btn.png"];
}else if (j == 4){
//图片
img.image = [UIImage imageNamed:@"btn.png"];
}
}
通过上图,打开离屏渲染的选项之后,可以看出10种测试,我们都设置了圆角+ clipsToBounds/masksToBounds,为什么有的触发了离屏渲染,有的没有?
首先,我们来结合CALayer的层级关系和cornerRadius的官方介绍分析一下:
- CALayer由backgroundColor(背景颜色层)、contents(内容层)、border(边框属性层)构成。
- 而cornerRadius的文档中明确说明:设置了cornerRadius,只对 CALayer 的backgroundColor和borderWidth&borderColor起作用,如果contents有内容或者内容的背景不是透明的话,只有设置masksToBounds为 true 才能起作用,此时两个属性相结合,产生离屏渲染。
- 那么我们看代码中:
1、针对UIButton,只要是 图片+ clipsToBounds(即masksToBounds)的情况,都会触发离屏渲染
2、针对UIImageView,只有 图片+背景色/边框+ masksToBounds,才会触发离屏渲染
【这里我们要看一下iOS官方针对UIImageView做的一些优化:
1、在iOS9之前,UIImageView和UIButton通过cornerRadius+masksToBounds/clipsToBounds设置圆角都会触发离屏渲染,
2、在iOS9以后,针对UIImageView中的image设置圆角并不会触发离屏渲染,如果加上了背景色或者阴影等其他效果还是会触发离屏渲染的】
这样我们就解释的通了。
那么我们这里的contents仅仅指的是图片吗?
其实并不是,于是笔者尝试了以下代码,总结出,contents也可以是有色信息(颜色、图片)的子视图
for (int i = 0; i < 3; i++) {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.frame = CGRectMake(50, 150*i + 50, 100, 100);
btn.backgroundColor = [UIColor redColor];
btn.layer.cornerRadius = 50;
btn.clipsToBounds = YES;
[self.view addSubview:btn];
if (i == 0) {
//无颜色
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(0, 0 , 50, 50);
btn1.backgroundColor = [UIColor clearColor];
[btn addSubview:btn1];
}else if (i == 1){
//有颜色
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(0, 0 , 50, 50);
btn1.backgroundColor = [UIColor blackColor];
[btn addSubview:btn1];
}else if (i == 2){
//有图片
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(0, 0 , 50, 50);
[btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
[btn addSubview:btn1];
}
所以,针对圆角如何避免触发离屏渲染,我们可以根据上述条件,根据自身项目需求进行特殊定制
5、layer.mask (遮罩/蒙版)
我们来看一下mask的渲染逻辑
如图:
- 系统先计算好mask部分,然后保存到离屏缓冲区
- 计算layer部分,计算好之后保存到离屏缓冲区
- 对mask和layer进行合并剪裁计算,最后结果提交到FrameBuffer,展示到屏幕上
所以说:
mask是覆盖在所有layer及其子layer之上的,可能还带有一定的透明度。
mask也是需要等整个layer树绘制完成,再加上mask和组合后的lzyer进行组合,所以需要开辟一个独立于FrameBuffer的内存,用于将layer及其子layer画完,最后再和mask进行组合,存储到FrameBuffer,视频控制器从FrameBuffer中读取数据显示到屏幕上
优化方案:不使用mask,使用混合图层,在layer上方叠加相应mask形状的半透明layer
6、组透明度(layer.allowsGroupOpacity / layer.opacity)
1、groupOpacity中alpha并不是分别应用到每一层之上,需要整个layer树画完之后,在统一加上alpha,和底层其他layer的像素进行组合,此时显然无法通过一次遍历就得到结果
2、需要另外开启一个独立内存,先将layer及其子layer画好,最后给组合后的图层加上alpha进行渲染,将最终结果存储到帧缓冲区
3、GroupOpacity 开启离屏渲染的条件是:layer.opacity != 1.0并且有子 layer 或者背景图。
另外,两个半透明的view,通过addSubView方法叠加,也会产生离屏渲染。
优化方案:关闭allowsGroupOpacity属性,根据产品需求自己控制layer透明度
那么
总结一下,常见的触发情况
1、使用了 mask 的 layer (layer.mask)
2、需要进行裁剪的 layer (layer.masksToBounds / view.clipsToBounds) ,同时拥有多层layer需要处理的情况
3、设置了组透明度为 YES,并且透明度不为 1 的 layer(layer.allowsGroupOpacity/layer.opacity)
4、添加了阴影 (layer.shadow)
5、采用了光栅化 (layer.shouldRasterize)
6、绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)
7、使用了高斯模糊
8、使用了抗锯齿(edge antialiasing)【allowsEdgeAntialiasing = YES】
三、离屏渲染与性能优化
1、离屏渲染的好处
- 为了特殊效果,不得不使用。例如系统自动触发的情况:圆角、阴影、高斯模糊、光栅化
- 提升效率。如果一个效果需要多次用到,我们可以提前渲染保存在offscreenbuffer中,免去重复计算的时间,达到复用的目的。这需要手动触发
2、 如何避免离屏渲染做到性能优化
- 圆角:虽然并不是所有的圆角+裁剪都会触发,但是我们也要分情况使用,可以使用切好的圆角图片,或者自己使用贝塞尔曲线进行圆角绘制
- 透明度:多层级的视图添加,不要设置透明度;不要设置组透明度
- 光栅化:当不存在短时间内需要反复多次大量复用的layer时,shouldRasterize设置为NO
- 阴影:增加阴影路径
- mask:使用混合图层,在layer上方叠加相应mask形状的半透明layer
- 抗锯齿:不开启 allowsEdgeAntialiasing 属性 (默认为NO)