一、什么是离屏渲染

在正常情况下,经过CPU的计算以及GPU的渲染之后,会将结果存放到帧缓存区,随后视频控制器会读取帧缓存区的数据,经过数模转换,再逐行显示到屏幕上
image.png
一般情况下,会遵循‘画家算法’(油画算法)按次序由远及近的一层一层将结果放置到帧缓存区中,当前帧缓存区的数据显示到屏幕上之后,就会将该帧丢弃,周而复始
image.png
但是,在某些特殊情况下(多图层的view设置了圆角等),当我们一层层的渲染完图层,要应用一些操作,比如裁剪的时候,之前放置在帧缓存区的数据早已经被丢弃或者说是被覆盖了,这个时候也就不可能对所有图层做这些操作了。
因此,我们需要开辟一些离屏缓存区来存放一些中间状态的数据,等待全部的图层都渲染到离屏缓存区之后,分别从各个离屏缓存区取出数据,分别做相应的操作(裁剪等)之后,组合存入帧缓存区,再等待屏幕控制器的读取和屏幕刷新。

image.png

二、离屏渲染的优劣

劣势

离屏渲染其实是加大了系统的负担,确实会造成性能上的损耗。

  • 离屏渲染需要额外的存储空间,上限是2.5倍的屏幕像素大小,一旦超过,则无法使用离屏渲染
  • 容易掉帧:一旦因为离屏渲染导致最终存入帧缓存区的时候,已经超过了16.67ms,则会出现掉帧的情况

    优势

    虽然离屏渲染会需要多开辟出新的临时缓存区来存储中间状态,但是对于多次出现在屏幕上的数据,可以提前渲染好,从而达到复用的目的,这样CPU/GPU就不用做一些重复的计算。
    其实在很多iOS开发的需求背景之下,比如 一些特殊动画效果的开发,此时需要多图层以及离屏缓存区保存中间状态,这种情况下就不得不使用离屏渲染。

    三、离屏渲染的检测

  • 可以通过在模拟器上,Debug-> Color Off-Screen Rendered

  • 其中出现黄色背景的,则为触发了离屏渲染

image.png

四、离屏渲染和常规渲染流程的区别

常规渲染

  1. 常规渲染:App -> FrameBuffer -> Display
  2. 当系统绘制好需要展示的图形之后,直接提交到帧缓冲区,然后显示在屏幕上就可以,
  3. 每次帧缓冲区绘制完成后,显示到屏幕时,原来的帧缓冲区中内容立即抛弃,进行下一次渲染

image.png

离屏渲染

  1. 离屏渲染:App -> OffScreenBuffer(进行计算合并等操作) -> FrameBuffer -> Display
  2. 当需要显示的图片由多个图层组合,一次绘制完成不了的时候,那么就需要在每个图层绘制好之后,保存到OffScreenBuffer中去,
  3. 最后当所有图层的内容全部绘制完成,再组合计算,把最后需要显示的内容提交到FrameBuffer中,然后显示到屏幕

image.png
下图是mask+layer发生离屏渲染的简易流程图
image.png
① 计算好mask部分,储存到离屏缓冲区。
② 计算好layer部分,储存到离屏缓冲区。
③ 在离屏渲染缓冲区里,对mask和layer进行合并剪裁计算,最后结果提交到FrameBuffer,展示到屏幕上

下图是层级较多的离屏渲染(实现一个毛玻璃效果)
image.png

  1. 渲染内容 Render Content
  2. 捕获内容 capture Content
  3. 水平模糊 Horizontal Blur
  4. 垂直模糊 Vertical Blue
  5. 合成过程,然后提交 Compositing Pass

五、哪些情况会出现离屏渲染

1.使用了 masks

masklayer 作为遮罩,显示在其所在的大layer以及大layer的所有子sublayer之上。masklayer可能也会带有透明度、形状(例如,显示指定区域内的的内容)等。
image.png
面对上面的一种情况,我们必须在离屏渲染缓冲区内完成Image和Mask的裁切合并处理,才能将最终的Masked Image -> 帧缓冲区显示。

2.毛玻璃模糊效果(高斯模糊

3.阴影效果(shadow)

shadow是一个矩形,是一个背景色,是layer的背景,所以是在layer的下面。shadow是根据layer而来,所以要先知道layer才能直到shadow的大小位置。
如果没有离屏渲染,和前文一样,按照画家算法,必须先将shadow放入帧缓存区,先显示。但是layer没有,不可能先渲染出shadow,只能利用离屏渲染缓冲区,等待shadow、layer等渲染并合并完成后,再送入帧缓存区等待显示。

4.组透明度(group opacity)

有很多sublayer,当我们对大的layer设置alpha时,会首先在离屏渲染缓冲区等待整个layer里面的sublayer全部完成后,再根据组透明度opacity计算新的颜色,再和下面的layer颜色整合,才会给帧缓冲区等待显示。 所以并不是每渲染一层sublayer就立马给显示。如果opacity为1则不需要调整透明度,正常画家算法显示。

5.光栅化(shouldRasterize)

  1. /* When true, the layer is rendered as a bitmap in its local coordinate
  2. * space ("rasterized"), then the bitmap is composited into the
  3. * destination (with the minificationFilter and magnificationFilter
  4. * properties of the layer applied if the bitmap needs scaling).
  5. * Rasterization occurs after the layer's filters and shadow effects
  6. * are applied, but before the opacity modulation. As an implementation
  7. * detail the rendering engine may attempt to cache and reuse the
  8. * bitmap from one frame to the next. (Whether it does or not will have
  9. * no affect on the rendered output.)
  10. *
  11. * When false the layer is composited directly into the destination
  12. * whenever possible (however, certain features of the compositing
  13. * model may force rasterization, e.g. adding filters).
  14. *
  15. * Defaults to NO. Animatable. */
  16. @property BOOL shouldRasterize;

如果开启,会将layer最后渲染,包括阴影、裁切等的最终效果变成位图放入离屏缓冲区,等待复用。但是,离屏缓冲区的大小不能超过屏幕的2.5倍,否则被释放;其次,layer如果不是静态的,比如imageview的image需要改变,label的text会发生改变等会发生频繁改变的,开启shouldRasterize离屏渲染会影响效率;离屏缓冲区是有时间限制的,超过100ms如果没有被使用,也会被释放。

所以,我们要善用shouldRasterize:

  • 如果layer不是静态的,不建议开启
  • 如果layer不能被复用,也不建议开启
  • 超过100ms没有被复用,也不建议开启
  • 超出屏幕像素的2.5倍,也不建议开启

6.圆角裁切(不一定会发生离屏渲染)

苹果官方文档对于cornerRadius的描述:

Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.

设置cornerRadius大于0时,只为layerbackgroundColorborder设置圆角;而不会对layercontents设置圆角,除非同时设置了layer.masksToBoundstrue(对应UIViewclipsToBounds

  1. UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200.0, 200.0)];
  2. // 设置背景色
  3. view1.backgroundColor = UIColor.redColor;
  4. // 设置边框宽度和颜色
  5. view1.layer.borderWidth = 2.0;
  6. view1.layer.borderColor = UIColor.blackColor.CGColor;
  7. // 设置圆角
  8. view1.layer.cornerRadius = 100.0;
  9. view1.center = self.view.center;
  10. [self.view addSubview:view1];

以上代码设置了 背景色 边框 圆角,并没有发生离屏渲染

我们再设置layer.masksToBounds或者clipsToBoundstrue,同样的没有触发离屏渲染。这是因为我们还没有设置图片,不光是图片,我们为视图添加一个有颜色、内容或边框等有图像信息的子视图也会触发离屏渲染。
image.png
本来我们从后往前绘制,绘制完一个图层就可以丢弃了。但现在有背景色,有图片内容,有边框,需要依次在Offscreen Buffer中保存,等待圆角+裁剪处理,就引发了离屏渲染

7.绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)

8.抗锯齿(allowsEdgeAntialiasing)

设置 allowsEdgeAntialiasing 属性为YES(默认为NO)