三、离屏渲染,你真的知道了吗?

一、首先让我们什么是离屏渲染

离屏渲染就是在当前屏幕缓冲区以外,新开辟一个缓冲区进行操作。

离屏渲染出发的场景有以下:
  • 圆角 (maskToBounds并用才会触发,contents内有内容)
  • 图层蒙版
  • 阴影
  • 光栅化
为什么要有离屏渲染?

大家高中物理应该学过显示器是如何显示图像的:需要显示的图像经过CRT电子枪以极快的速度一行一行的扫描,扫描出来就呈现了一帧画面,随后电子枪又会回到初始位置循环扫描,形成了我们看到的图片或视频。

为了让显示器的显示跟视频控制器同步,当电子枪新扫描一行的时候,准备扫描的时发送一个水平同步信号(HSync信号),显示器的刷新频率就是HSync信号产生的频率。然后CPU计算好frame等属性,将计算好的内容交给GPU去渲染,GPU渲染好之后就会放入帧缓冲区。然后视频控制器会按照HSync信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器,就显示出来了。具体的大家自行查找资料或询问相关专业人士,这里只参考网上资料做一个简单的描述。

离屏渲染的代价很高,想要进行离屏渲染,首选要创建一个新的缓冲区,屏幕渲染会有一个上下文环境的一个概念,离屏渲染的整个过程需要切换上下文环境,先从当前屏幕切换到离屏,等结束后,又要将上下文环境切换回来。这也是为什么会消耗性能的原因了。

由于垂直同步的机制,如果在一个 HSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

为什么要避免离屏渲染?

CPU GPU 在绘制渲染视图时做了大量的工作。离屏渲染发生在 GPU 层面上,会创建新的渲染缓冲区,会触发 OpenGL 的多通道渲染管线,图形上下文的切换会造成额外的开销,增加 GPU 工作量。如果 CPU GPU 累计耗时 16.67 毫秒还没有完成,就会造成卡顿掉帧。

圆角属性、蒙层遮罩 都会触发离屏渲染。指定了以上属性,标记了它在新的图形上下文中,在未愈合之前,不可以用于显示的时候就出发了离屏渲染。

二、屏幕上最终显示的数据有两种加载流程

  • On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作
  • Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
2251862-9f6a6db1c7de28c7.png

2.1 首先来讲讲正常渲染流程On-Screen Rendering

APP中的数据经过CPU计算和GPU渲染后,将结果存放在帧缓冲区,利用视频控制器从帧缓冲区中取出,并显示到屏幕上。

  • 在GPU的渲染流程中,显示到屏幕上的图像是遵循大画家算法按照由远及近的顺序,依次将结果存储到帧缓冲区
  • 视屏控制器从帧缓冲区中读取一帧数据,将其显示到屏幕上后,会立即丢弃这帧数据,不会做任何保留,这样做的目的是可以节省空间,且在屏幕上是各自显示各自的,互相不影响。
2251862-f372c2ca8eed57ec.png

2.2 离屏渲染流程Off-Screen Rendering

当App需要进行额外的渲染和合并时,例如按钮设置圆角,我们是需要对UIButton这个控件中的所有图层都进行圆角+裁剪,然后再将合并后的结果存入帧缓存区,再从帧缓存中取出交由屏幕显示,这时,在正常的渲染流程中,我们是无法做到对所有图层进行圆角裁剪的,因为它是用一个丢一个。所以我们需要提前将处理好的结果放入离屏缓冲区,最后将几个图层进行叠加合并,存放到站缓冲区,最后屏幕上就是我们想实现的效果。

2251862-74cbcd310d5c26a4.png

说白了,离屏缓存区就是一个临时的缓冲区,用来存放在后续操作使用,但目前并不使用的数据。

  • 离屏渲染再给我们带来方便的同时,也带来了严重的性能问题。由于离屏渲染中的离屏缓冲区,是额外开辟的一个存储空间,当它将数据转存到Frame Buffer时,也是需要耗费时间的,所以在转存的过程中,仍有掉帧的可能。
  • 离屏缓冲区的空间并不是无限大的, 它是又上限的,最大只能是屏幕的2.5倍
2.2.1 离屏渲染消耗性能的原因

需要创建新的缓冲区
离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕

2.2.2 哪些操作会触发离屏渲染?
  • 光栅化,layer.shouldRasterize = YES
  • 遮罩,layer.mask
  • 圆角,同时设置 layer.masksToBounds = YES、layer.cornerRadius大于0
  • 考虑通过 CoreGraphics 绘制裁剪圆角,或者叫美工提供圆角图片
  • 阴影,layer.shadowXXX,如果设置了 layer.shadowPath 就不会产生离屏渲染
2.2.3 那为什么我们明知有性能问题时,还是要使用离屏渲染呢?
  • 可以处理一些特殊的效果,这种效果并不能一次就完成,需要使用离屏缓冲区来保存中间状态,不得不使用离屏渲染,这种情况下的离屏渲染是系统自动触发的,例如经常使用的圆角、阴影、高斯模糊、光栅化等
  • 可以提升渲染的效率,如果一个效果是多次实现的,可以提前渲染,保存到离屏缓冲区,以达到复用的目的。这种情况是需要开发者手动触发的。
离屏渲染的另一个原因:光栅化

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.

当我们开启光栅化时,会将layer渲染成位图保存在缓存中,这样在下次使用时,就可以直接复用,提高效率。
针对光栅化的使用,有以下几个建议:

  • 如果layer不能被复用,则没有必要开启光栅化
  • 如果layer不是静态,需要被频繁修改(例如动画过程中),此时开启光栅化反而影响效率
  • 离屏渲染缓存内容有时间限制,如果100ms内没有被使用,那么就会丢弃,无法进行复用
  • 离屏渲染的缓存空间有限,是屏幕的2.5倍,超过2.5倍屏幕像素大小的话也会失效,无法实现复用

三、圆角中离屏渲染的触发时机

在讲圆角之前,首先说明下CALayer的构成,如图所示,它是backgrodColorcontentsborderWidth&borderColor构成的。跟我们即将解释的圆角触发离屏渲染息息相关。

2251862-9f16cc22ac8e8ee5.png

首先我们先开启离屏渲染的检测,在模拟器打开color offscreen-rendered,开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。

22813661-3d3d8edcef335637.png

我们写下如下代码:

  let btn = UIButton(type: .custom)
        btn.frame = CGRect(x: 100, y: 200, width: 100, height: 100)
        //设置圆角
        btn.layer.cornerRadius = 50
        //设置border宽度和颜色
        btn.layer.borderWidth = 2
        btn.layer.borderColor = UIColor.red.cgColor
        self.view.addSubview(btn)
        //设置背景图片
        btn.setImage(UIImage(named: "update_header"), for: .normal)

截屏2020-07-08 下午1.35.04.png

此时我们发现图片并没有变成圆形还是一个正方形
针对上面的这个问题,我相信99%的人都能信手拈来,知道必须要设置masksToBoundstrue,才会得到我们想要的效果。解决的方法很简单,但原理是大部人都没有去仔细研究的。

其实苹果的官方文档有告诉我们,设置cornerRadius只会对CALayer中的backgroundColorboder设置圆角,不会设置contents的圆角,如果contents需要设置圆角,需要同时将maskToBounds / clipsToBounds设置为true
所以我们理解为圆角不生效的根本原因是没有对contents设置圆角,而按钮设置的image是放在contents里面的,所以看到的界面上的就是image没有进行圆角裁剪。

然而当我们加上maskToBounds / clipsToBounds修改为true时,会出现以下情况,说明此时触发了离屏渲染

截屏2020-07-08 下午1.41.04.png

那为啥会触发离屏渲染呢?

是因为圆角的设置是需要对所有layer都进行裁剪的,而maskToBounds裁剪是应用到所有layer上的。如果从正常渲染的角度来说,一个个layer是用完即扔的。而现在我们的圆角设置需要3个layer叠加合并的,所以将先处理好的layer保存在离屏缓冲区,等到最后一个layer处理完,合并进行圆角+裁剪,所以才会触发离屏渲染

所以我们可以做以下总结
  • 当只设置backgroundColorborde,而contents中没有子视图时,无论maskToBounds / clipsToBoundstrue还是false,都不会触发离屏渲染
  • contents中有子视图时,此时设置 cornerRadius+maskToBounds / clipsToBounds,就会触发离屏渲染

你可能感兴趣的:(三、离屏渲染,你真的知道了吗?)