七、深入剖析【离屏渲染】原理

891 阅读11分钟

离屏渲染与正常渲染

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

  • 正常渲染加载流程

  • 离屏渲染加载流程

    image

从图上看,他们之间的区别就是离屏渲染比正常渲染多了一个离屏缓冲区,这个缓冲区的作用是什么呢?下面来仔细说说

首先,说说正常渲染流程

正常渲染流程

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

  • 在GPU的渲染流程中,显示到屏幕上的图像是遵循大画家算法按照由远及近的顺序,依次将结果存储到帧缓冲区

  • 视屏控制器从帧缓冲区中读取一帧数据,将其显示到屏幕上后,会立即丢弃这帧数据,不会做任何保留,这样做的目的是可以节省空间,且在屏幕上是各自显示各自的,互相不影响。

    image

离屏渲染流程

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

image

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

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

那为什么我们明知有性能问题时,还是要使用离屏渲染呢?

  • 可以处理一些特殊的效果,这种效果并不能一次就完成,需要使用离屏缓冲区来保存中间状态,不得不使用离屏渲染,这种情况下的离屏渲染是系统自动触发的,例如经常使用的圆角、阴影、高斯模糊、光栅化等
  • 可以提升渲染的效率,如果一个效果是多次实现的,可以提前渲染,保存到离屏缓冲区,以达到复用的目的。这种情况是需要开发者手动触发的。

离屏渲染的另一个原因:光栅化

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的构成,如图所示,它是由backgroundColor、contents、borderWidth&borderColor构成的。跟我们即将解释的圆角触发离屏渲染息息相关。

image

圆角设置不生效问题!

在平常写代码时,比如UIButton设置圆角,当设置好按钮的image、cornerRadius、borderWidth、borderColor等属性后,运行发现并没有实现我们想要的效果

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

此时的效果就是这样的,可以发现,我们设置的按钮图片还是方方正正的

image

针对上面的这个问题,我相信99%的人都能信手拈来,知道必须要设置masksToBounds为 true,才会得到我们想要的效果。解决的方法很简单,但原理是大部人都没有去仔细研究的。

下面是苹果官方文档针对圆角设置的一些说明:

image

官方文档告诉我们,设置cornerRadius只会对CALayer中的backgroundColor 和 boder设置圆角,不会设置contents的圆角,如果contents需要设置圆角,需要同时将maskToBounds / clipsToBounds设置为true。

所以我们可以理解为圆角不生效的根本原因是没有对contents设置圆角,而按钮设置的image是放在contents里面的,所以看到的界面上的就是image没有进行圆角裁剪。

下面我们通过几段代码来说明 圆角设置中什么时候会离屏渲染触发 首先,需要打开模拟器的离屏渲染颜色标记

image

1、按钮 仅设置背景颜色+border

        let btn01 = UIButton(type: .custom)
        btn01.frame = CGRect(x: 100, y: 200, width: 100, height: 100)
        //设置圆角
        btn01.layer.cornerRadius = 50
        //设置border宽度和颜色
        btn01.layer.borderWidth = 4
        btn01.layer.borderColor = UIColor.red.cgColor
        self.view.addSubview(btn01)
        //设置背景颜色
        btn01.backgroundColor = UIColor.green

在这种情况下,无论是使用默认的maskToBounds / clipsToBounds(false),还是将其修改为true,都不会触发离屏渲染,究其根本原因是 contents中没有需要圆角处理的layer

image

情况2:按钮设置背景图片+boder

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

  • 使用默认的maskToBounds / clipsToBounds(false) 这种情况就是最开始我们讲到的圆角设置不生效的情况,就不再多做说明了

  • maskToBounds / clipsToBounds 修改为true

    image

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

情况3 btn.layer.cornerRadius = 50&&btn.clipsToBounds = YES就一定会触发离屏渲染? 首先我们先开启离屏渲染的检测,在模拟器打开color offscreen-rendered,开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。

image

我们写下如下代码:

image

注意:在我的电脑中,选择机型11-Pro-Max的时候出现的模拟器屏幕是:

image

细心的同学就会发现1和3变成了黄色,这里看不清楚,当我选择了iphone8的时候:

image

这里就明显看出1和3变成了黄色,标记为触发了离屏渲染,个人觉得这应该是模拟器的bug吧,如果你的电脑没有出现这个问题,请忽略,有的话就试着选一选其他机型吧!!!

首先普及一下CALayer的层次结构:CALayer由背景色backgroundColor、内容contents、边缘borderWidth&borderColor构成

image

**重点重点重点(重要的事情说三遍):**cornerRadius的文档中明确说明对cornerRadius的设置只对 CALayer 的backgroundColor和borderWidth&borderColor起作用,如果contents有内容或者内容的背景不是透明的话,只有设置masksToBounds为 true 才能起作用,此时两个属性相结合,产生离屏渲染。这也就说明了上面代码为什么1和3触发了离屏渲染,而2和4没有触发离屏渲染

解决办法:

(1)后台绘制圆角图片,前台进行设置

image

image

(2)对于 contents 无内容或者内容的背景透明(无涉及到圆角以外的区域)的layer,直接设置layer的 backgroundColor 和 cornerRadius 属性来绘制圆角。

(3)使用混合图层,在layer上方叠加相应mask形状的半透明layer

sublayer.contents=(id)[UIImage imageNamed:@"xxx"].CGImage;

[view.layer addSublayer:sublayer];

(4)- (UIImage *)yy_imageByRoundCornerRadius:(CGFloat)radius corners:(UIRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(UIColor *)borderColor borderLineJoin:(CGLineJoin)borderLineJoin此方法为YY_image处理圆角的方法,你可以去下载YY_image查看源码

其他情况触发离屏渲染以及解决办法:

1. mask(遮罩)------>使用混合图层,在layer上方叠加相应mask形状的半透明layer

2.edge antialiasing(抗锯齿)----->不设置 allowsEdgeAntialiasing 属性为YES(默认为NO)

3. allowsGroupOpacity(组不透明,开启CALayer的allowsGroupOpacity属性后,子 layer 在视觉上的透明度的上限是其父 layer 的opacity(对应UIView的alpha),并且从 iOS 7 以后默认全局开启了这个功能,这样做是为了让子视图与其容器视图保持同样的透明度。)------->关闭 allowsGroupOpacity 属性,按产品需求自己控制layer透明度

4.shadows(阴影)------>设置阴影后,设置CALayer的 shadowPath,view.layer.shadowPath=[UIBezierPath pathWithCGRect:view.bounds].CGPath;

CALayer离屏渲染终极解决方案:当视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES(缓存离屏渲染的数据,当下次用到的时候直接拿,不需要开辟新的离屏缓冲区),此方案最为实用方便。view.layer.shouldRasterize = true;view.layer.rasterizationScale = view.layer.contentsScale;

shouldRasterize (光栅华使用建议):

1.如果layer不需要服用,则没有必要打开

2.如果layer不是静态的,需要被频繁修改,比如出于动画之中,则开启光栅华反而影响性能

3.离屏渲染缓存有时间限制,当超过100ms,内容没有被使用就会被丢弃,无法复用

4.离屏渲染缓存有空间限制,超过屏幕像素的2.5倍则失效,并无法使用

特别说明:当视图内容是动态变化(如后台下载图片完毕后切换到主线程设置)时,使用此方案反而为增加系统负荷。

总结:

(1)离屏渲染是系统触发,触发了之后才有离屏缓冲区,离屏缓冲区和我上一篇文章讲到的帧缓冲区的二级缓冲机制没有任何的因果关系

(2)btn.layer.cornerRadius = 50&&btn.clipsToBounds = YES不一定会触发离屏渲染,cornerRadius的文档中明确说明对cornerRadius的设置只对 CALayer 的backgroundColor和borderWidth&borderColor起作用,如果contents有内容或者内容的背景不是透明的话,只有设置masksToBounds为 true 才能起作用,此时两个属性相结合,产生离屏渲染

(3)在uitableVIewcell触发了离屏渲染,会导致在滑动的时候高频率的开辟离屏缓冲区,这样就会造成tanleView滑动卡顿,如果视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES,此方案最为实用方便,但是当视图内容是动态变化(如后台下载图片完毕后切换到主线程设置)时,使用此方案反而为增加系统负荷。

(4)现在摆在我们面前得有三个选择:当前屏幕渲染、离屏渲染、CPU渲染,该用哪个呢?这需要根据具体的使用场景来决定。·   尽量使用当前屏幕渲染,鉴于离屏渲染、CPU渲染可能带来的性能问题,一般情况下,我们要尽量使用当前屏幕渲染。离屏渲染 VS CPU渲染

总结

  • 当只设置backgroundColor、border,而contents中没有子视图时,无论maskToBounds / clipsToBoundstrue还是false,都不会触发离屏渲染
  • 当contents中有子视图时,此时设置 cornerRadius+maskToBounds / clipsToBounds,就会触发离屏渲染,但是这种情况在UIImageView中并不适用,当UIImageView中只设置图片+maskToBounds / clipsToBounds是不会触发离屏渲染,苹果对UIImageView优化我想也只是将image直接画在了contents上面这样不设置背景色其实只需要渲染一个layer,所以不需要用到离屏缓冲区,,(在这里感谢下das112 童鞋对这段描述更清晰的解释,这里确实写的有点虎头蛇尾的),所以不会产生离屏渲染,如果此时再加上背景色,就会触发离屏渲染。

所以,综合来说,离屏渲染是否触发,在于我们是否需要使用离屏缓冲区