参考链接:
一、定义
iOS页面显示流程是,CPU将需要显示的内容进行处理,例如顶点、图元、颜色等信息,处理完成后交给GPU进行渲染,渲染后放在一个和屏幕像素数据量一样大的缓存空间frame buffer里面,这也就是GPU存储渲染结果的地方。
但是有时候有多个图层,当对下面图层做一些处理,例如圆角,并且要求上面子图层需要跟父图层做一样的处理,这个时候就需要将每个图层的渲染结果存储在另外的内存区域offscreenBuffer,然后所有图层都渲染完成后,作为处理后再将结果写入到frame buffer里面,这个过程就被称之为离屏渲染。

二、离屏渲染的检测方式
- 可以通过在模拟器上,Debug-> Color Off-Screen Rendered
- 其中出现黄色背景的,则为触发了离屏渲染

三、离屏渲染的触发方式
1、毛玻璃以及shouldRasterize光栅化
a、代码
CGFloat y = 60;
// 触发方式1: 毛玻璃效果
UIButton *btn0 = [UIButton buttonWithType:UIButtonTypeCustom];
btn0.frame = CGRectMake(0, y, 50, 50);
[self.view addSubview:btn0];
[btn0 setImage:[UIImage imageNamed:@"BYLaunchPrivacyImage"] forState:UIControlStateNormal];
UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:effect];
//必须给effcetView的frame赋值,因为UIVisualEffectView是一个加到UIIamgeView上的子视图.
effectView.frame = CGRectMake(0, 0, 30, 30);
[btn0 addSubview:effectView];
// 触发方式2: shouldRasterize
UIButton *btn_1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn_1.frame = CGRectMake(100, y, 50, 50);
btn_1.layer.shouldRasterize = YES;
[btn_1 setImage:[UIImage imageNamed:@"BYLaunchPrivacyImage"] forState:UIControlStateNormal];
[self.view addSubview:btn_1];
// 触发方式2: shouldRasterize
UIButton *btn_2 = [UIButton buttonWithType:UIButtonTypeCustom];
btn_2.frame = CGRectMake(200, y, 50, 50);
[btn_2 setImage:[UIImage imageNamed:@"BYLaunchPrivacyImage"] forState:UIControlStateNormal];
[self.view addSubview:btn_2];
b、效果

我们发现:
- 当我们仅仅给btn设置image的话是没有离屏渲染的,说明当我们给btn设置image时候相当于在btn上面加了一个imageView,这时候就相当btn上面有两个layer
- 当我们给btn加上毛玻璃效果时候,毛玻璃效果地方出现了离屏渲染
- 当我们设置了shouldRasterize时候,btn也出现了离屏渲染
2、圆角+maskToBounds
a、代码
// 触发方式3: 圆角+maskToBounds
//UIImageView 设置了图片+背景色;
UIImageView *img1 = [[UIImageView alloc]init];
img1.frame = CGRectMake(100, 200, 50, 50);
img1.backgroundColor = [UIColor blueColor];
[self.view addSubview:img1];
img1.layer.cornerRadius = 10;
img1.layer.masksToBounds = YES;
img1.image = [UIImage imageNamed:@"BMC_Common_EditBtn"];
//UIImageView 只设置了图片,无背景色;
UIImageView *img2 = [[UIImageView alloc]init];
img2.frame = CGRectMake(100, 260, 50, 50);
img2.image = [UIImage imageNamed:@"BMC_Common_EditBtn"];
[self.view addSubview:img2];
img2.layer.cornerRadius = 10;
img2.layer.masksToBounds = YES;
//UIImageView 只设置了图片,无背景色;
UIImageView *img3 = [[UIImageView alloc]init];
img3.frame = CGRectMake(100, 320, 50, 50);
img3.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:img3];
img3.layer.cornerRadius = 10;
img3.image = [UIImage imageNamed:@"BMC_Common_EditBtn"];
b、效果

我们发现:
- 当设置了背景颜色和image的同时,再加上cornerRadius和masksToBounds,会触发离屏渲染
- 如果只设置了image,加上加上cornerRadius和masksToBounds不会触发离屏渲染,根据第一条,说明当设置了backgroundColor和image,imageView拥有了两个图层,其中background一个layer,image一个layer,其实每个layer就是一个图层
- 假如设置了背景颜色和image,如果只有cornerRadius,但是没有masksToBounds,不会触发离屏渲染,这是因为没设置masksToBounds,那圆角只是针对imageView一个图层有效,对于其他图层无用
3、父view和子view都有透明度
a、代码
//4.父view和子view都有透明度;
UIView *v1 = [[UIView alloc]init];
v1.frame = CGRectMake(50, 500, 100, 100);
v1.layer.cornerRadius = 50;
v1.backgroundColor = [UIColor colorWithRed:1 green:0.3 blue:0.2 alpha:1];
v1.alpha = 0.5;
[self.view addSubview:v1];
UIView *v2 = [[UIView alloc]init];
v2.frame = CGRectMake(30, 30, 50, 50);
v2.layer.cornerRadius = 20;
v2.backgroundColor = [UIColor colorWithRed:0.3 green:1 blue:0.2 alpha:1];
v2.alpha = 0.5;
[v1 addSubview:v2];
b、效果
p1-jj.byteimg.com/tos-cn-i-t2…

我们发现触发了离屏渲染,但是假如父视图有两个子view。并且两个子view都有透明度且叠加在一起,这样不会触发离屏渲染
三、离屏渲染的原理
当有多个图层叠加在一起的时候,绘制一帧图形的时候会先绘制场景中离观察者较远的物体,在绘制较近的物体,例如下图:

先绘制红色部分,再绘制⻩色部分,最后再绘制灰⾊部分,即可解决隐藏面消除的问题。即将场景按照物理距离和观察者的距离远近排序,由远及近的绘制即可,这个时候就是普通渲染
但是当我们设置了cornerRadius和masksToBounds进行圆角+裁剪时,masksToBounds裁剪属性会应用到所有的图层上。

本来我们从后往前绘制时候,绘制完一个图层就可以将该图层丢弃了,但现在需要依次在 Offscreen Buffer中保存,等待圆角+裁剪处理,即引发了 离屏渲染 。

但是根据文档我们知道:
-
背景色、边框、背景色+边框,再加上圆角+裁剪,根据文档说明,因为 contents = nil 没有需要裁剪处理的内容,所以masksToBounds设置为YES或者NO都没有影响。
-
一旦我们 为contents设置了内容 ,无论是图片、绘制内容、有图像信息的子视图等,再加上圆角+裁剪,就会触发离屏渲染。
也就是说如果只有一个layer,那么无论masksToBounds设置为YES或者NO都不会产生离屏渲染,但是毛玻璃效果本身是在原view上面加一个子view,所以不是一个layer
1、普通渲染
- 普通渲染:App -> FrameBuffer -> Display
- GPU将每一个subLayer绘制完成后就直接把subLayer丢弃,丢弃了就不能再对绘制完的图案就行处理了
- 当所有subLayer都绘制完了后,就形成了一帧屏幕,然后将该帧屏幕缓存到frameBuffer中,等屏幕去显示

2、离屏渲染
- 离屏渲染:App -> Off-Screen Buffer(进行计算合并等操作) -> FrameBuffer -> Display
- 因为触发离屏渲染是因为需要等所有的subLayer绘制完后再进行mask或者cornerRadius等处理,处理后再缓存到frameBuffer中
- 所有每个subLayer绘制完后需要缓存到offscreen中,等所有subLayer绘制完成后,再对offscreen里面的subLayer进行组合处理,处理完成后放到frameBuffer中


3、离屏渲染流程

- 系统先计算好mask部分,然后保存到离屏缓冲区
- 计算layer部分,计算好之后保存到离屏缓冲区
- 对mask和layer进行合并剪裁计算,最后结果提交到FrameBuffer,展示到屏幕上
四、离屏渲染的性能优化
1、 需要优化的原因
- 离屏渲染需要单独开辟一个缓存区域offscreenBuffer
- 将sublayer缓存到offscreenBugger需要额外的存储时间
- offscreenBuffer的缓存的空间有限制,大小在屏幕像素的2.5倍
- 一旦缓存超过100ms没有被使用,会自动被丢弃
- 额外的处理会造成CPU性能的损耗
GPU的操作是高度流水线化的。本来所有计算工作都在有条不紊地正在向frame buffer输出,此时突然收到指令,需要输出到另一块内存,那么流水线中正在进行的一切都不得不被丢弃,切换到只能服务于我们当前的“切圆角”操作。等到完成以后再次清空,再回到向frame buffer输出的正常流程。
在tableView或者collectionView中,滚动的每一帧变化都会触发每个cell的重新绘制,因此一旦存在离屏渲染,上面提到的上下文切换就会每秒发生60次,并且很可能每一帧有几十张的图片要求这么做,对于GPU的性能冲击可想而知(GPU非常擅长大规模并行计算,但是我想频繁的上下文切换显然不在其设计考量之中)
2、优化的方案
尽管离屏渲染开销很大,但是当我们无法避免它的时候,可以想办法把性能影响降到最低,尽量只在下面几种情况触发:
- 1、没法一次性画出效果
- 2、特殊效果:需要使用额外的offscreenBuffer保存中间状态,不能不使用,系统自动触发,圆角、阴影
- 3、效率的优势:既然效果会多次屏幕,提前渲染结果保存在offscreenBuffer中,然后复用目的
- 4、对于view的圆形边框,如果没有backgroundColor,可以放心使用cornerRadius来做
- 5、对于特殊形状的view,使用layer mask并打开shouldRasterize来对渲染结果进行缓存
- 6、对于模糊效果,不采用系统提供的UIVisualEffect,而是另外实现模糊效果(CIGaussianBlur),并手动管理渲染结果