看上图得知Application以及RenderServer部分是运行在CPU上的,Application处理好数据后,提交到Render Server,然后Core Animation在会将具体操作转换成GPU的Draw calls(OpenGL/Metal);也就是CPU+GPU共同完成渲染工作。
离屏渲染(offscreen buffer)
如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染
画家算法
图层的叠加绘制大概遵循“画家算法”,
在普通的 layer 绘制中,上层的 sublayer 会覆盖下层的 sublayer,下层 sublayer 绘制完之后就可以抛弃了,从而节约空间提高效率。所有 sublayer 依次绘制完毕之后,整个绘制过程完成,就可以进行后续的呈现了。
正常渲染流程
APP -----> FrameBuffer(帧缓冲区) -----> Display显示
控制器从帧缓冲区读取一帧的数据将其显示后,就立刻丢弃了这一帧数据,然后进行下一帧的渲染显示。
这样做的好处是节省了空间。
离屏渲染流程
APP -----> OffScreenBuffer(离屏缓冲区) -----> FrameBuffer(帧缓冲区) -----> Display显示
当APP要进行额外的渲染和合并时(比如设置了圆角+裁剪),我们需要把不同的图层进行裁剪+合并的操作,这时就不能直接放入FrameBuffer了,我们要把渲染好的结果放入OffScreenBuffer,等待合适的机会将几个图层进行裁剪、合并叠加的操作,完成后把结果放入FrameBuffer中,由视频控制器显示到屏幕上
使用离屏渲染的原因:
1.非常多的特殊效果,并不能一次用一个图层就能画出来,所以需要使用额外的offscreen Buffer来保持中间状态(不得不使用),比如:圆角,阴影
2.能带来效率的优势:既然效果会多次出现在屏幕上,可提前渲染好,保存在offscreen Buffer中,从而达到复用的结果 比如:光珊化shouldRasterize
避免离屏渲染的原因:
1.离屏渲染时由于 App 需要提前对部分内容进行额外的渲染并保存到 Offscreen Buffer,以及需要在必要时刻对 Offscreen Buffer 和 Framebuffer 进行内容切换,所以会需要更长的处理时间(实际上这两步关于 buffer 的切换代价都非常大)
2.并且 Offscreen Buffer 本身就需要额外的空间,大量的离屏渲染可能造成内存的过大压力。与此同时,Offscreen Buffer 的总大小也有限,不能超过屏幕总像素的 2.5 倍。
3.可见离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,很容易造成掉帧的问题。所以大部分情况下,我们都应该尽量避免离屏渲染。
结合CALayer的层级关系和cornerRadius的官方介绍分析一下
1.CALayer由backgroundColor(背景颜色层)、contents(内容层)、border(边框属性层)
构成。
2.而cornerRadius的文档中明确说明:设置了cornerRadius,只对 CALayer 的backgroundColor和borderWidth&borderColor起作用,如果contents有内容或者内容的背景不是透明的话,只有设置masksToBounds为 true 才能起作用,此时两个属性相结合,产生离屏渲染。
下面通过案例来理解离屏渲染
1.按钮存在背景图片
UIButton * btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 100, 100, 100);
btn1.layer.cornerRadius = 50;
[btn1 setImage:[UIImage imageNamed:@"截屏2020-07-07 上午11.05.22"] forState:0];
[self.view addSubview:btn1];
btn1.clipsToBounds = YES;
分析:
1.同时设置了 cornerRadius 和 clipsToBounds 这两个属性,会对 layer 的三层都起作用;
2.button中设置了图片 setImage,说明它的contents中存在内容,需要渲染的内容有contents;(这里相当于给按钮添加了一个imageview)
3.对于复合图形的渲染,是需要借助 Offscreenbuffer 缓存的,所以触发了离屏渲染
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;
分析:
1.同时设置了 cornerRadius 和 clipsToBounds 这两个属性,会对 layer 的三层都起作用
2.而且还设置了 backgroundColor,
3.但是 btn2 中并没有 contents 的元素,只有本身的 backgroundColor,是一个单一图层,所以不会触发离屏渲染
3.不存在背景图片
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"];
分析:
1.backgroundColor:[UIColor blueColor];
2.contents: [UIImage imageNamed:@"btn.png"];
3.cornerRadius + masksToBounds 所以会离屏渲染
4.只设置图片无背景色
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.只有 contents: [UIImage imageNamed:@"btn.png"];
2.所以不会造成离屏渲染
总结:
1、针对UIButton,只要是 图片+ clipsToBounds(即masksToBounds)的情况,都会触发离屏渲染
2、针对UIImageView,只有 图片+背景色/边框+ masksToBounds,才会触发离屏渲染
这里我们要看一下iOS官方针对UIImageView做的一些优化:
1、在iOS9之前,UIImageView和UIButton通过cornerRadius+masksToBounds/clipsToBounds设置圆角都会触发离屏渲染,
2、在iOS9以后,针对UIImageView中的image设置圆角并不会触发离屏渲染,如果加上了背景色或者阴影等其他效果还是会触发离屏渲染的
高斯模糊的离屏渲染逻辑
1.Content:渲染内容
2.capture content:捕获内容
3.Horizontal Blur:水平模糊
4.Vertical Blur:垂直模糊
5.Compositing Pass:合并过程
6.合并完成之后将结果存入帧缓存区,等待下一次 runloop到来,显示到屏幕上
光栅化和离屏渲染的联系
从上面离屏渲染的原理中可以知道,如果我们只是单一的图层显示,是不会触发离屏渲染的,而当我们开启光栅化之后,不管是单一图层还是复合图层,都会触发离屏渲染。
所以光栅化的目的就是强制开启离屏渲染。
最后一行打开了光栅化,所以也开启了离屏渲染
引发离屏渲染的情况
1.使用了 mask 的 layer (layer.mask)
2.需要进行裁剪的 layer (layer.masksToBounds /view.clipsToBounds)
设置了layer.cornerRadius,只会设置backgroundColor和border的圆角,不会设置content的圆角,除非同时设置了layer.masksToBounds
3.设置了组透明度为 YES,并且透明度不为 1 的layer (layer.allowsGroupOpacity/ layer.opacity)
4.添加了投影的 layer (layer.shadow*)
5.采用了光栅化的 layer (layer.shouldRasterize)
1)如果layer不被复用,则没有必要打开光栅化
2)如果layer不是静态的,需要被频繁修改,比如处于动画之中,那么开启离屏渲染会影响动画效果了。
3)离屏渲染缓存有时间限制,缓存内容100ms内如果没有被使用,那么它就会丢弃,就无法再复用了。
4)离屏渲染缓存空间有限,超过2.5倍屏幕像素大小也会失效。且无法进行复用了。
6.绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)
如何避免离屏渲染做到性能优化
圆角:虽然并不是所有的圆角+裁剪都会触发,但是我们也要分情况使用,可以使用切好的圆角图片,或者自己使用贝塞尔曲线进行圆角绘制
透明度:多层级的视图添加,不要设置透明度;不要设置组透明度
光栅化:当不存在短时间内需要反复多次大量复用的layer时,shouldRasterize设置为NO
阴影:增加阴影路径
mask:使用混合图层,在layer上方叠加相应mask形状的半透明layer
抗锯齿:不开启 allowsEdgeAntialiasing 属性 (默认为NO)