iOS离屏渲染原理和优化

转自:https://blog.bombox.org/2020-07-14/ios-offscreen-render/

 

几乎做iOS开发的人都知道,设置圆角会触发离屏渲染,那么什么情况下设置圆角不会触发离屏渲染呢,为什么会触发离屏渲染。

油画算法

计算机图层的叠加绘制大概遵循油画算法,在这种算法下会按层绘制,首先绘制距离较远的场景,然后用绘制距离较近的场景覆盖较远的部分,如下图。

油画算法

这样就不会导致远的物体挡住近的物体,但是有个局限,就是无法在后面一层渲染完成后,再回去修改前面图层,因为前面的图层已经被覆盖了

离屏渲染

对于有前后依赖的图层(如全局剪切,阴影等),油画算法无法满足我们的需求,对于有前后依赖的图层,我们可以再另开辟一个空间,用于临时渲染,渲染完成后再渲染到当前的缓冲区上,这个临时渲染,就是离屏渲染,由于需要开辟一个新的内存空间,并且共享同一个上下文,所以还需要做上下文切换(状态切换),并且渲染完成后还要进行拷贝操作

  1. 开辟临时缓存空间
  2. 上下文切换,上下文对象比较大,切换操作会带来一定的性能消耗
  3. 内存拷贝
  4. 额外的渲染(没有进一步考证)

上面4项带来的开销会很大,并且每一帧渲染都需要执行,如果屏幕上触发离屏渲染的操作过多,会导致GPU渲染时间过长造成卡顿,应该避免触发离屏渲染

iOS圆角问题

官方文档关于layer.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 YES causes the content to be clipped to the rounded corners. 

The default value of this property is 0.0.

 


layer.cornerRadius只作用backgroundColorborder,不会作用于content,支持动画

离屏渲染是GPU无法按油画算法一次性渲染完我们的视图才会触发,我们先来看几个iOS的例子,模拟器打开Color Off-screen Rendered

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 1. UIImageView
let imageView = UIImageView(frame: CGRect(x: 50, y: 100, width: 300, height: 200))
self.view.addSubview(imageView)
imageView.image = UIImage.init(named: "test.jpg")

// image + cornerRadius + masksToBounds 不会触发离屏渲染
imageView.layer.cornerRadius = 10
imageView.layer.masksToBounds = true

// 触发离屏渲染
imageView.backgroundColor = UIColor.green
// 添加一个空的UIView不会触发离屏渲染
// imageView.addSubview(UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10)))

// 2. UIButton
let button = UIButton(type: .custom)
button.frame = CGRect(x: 50, y: 300 + 50, width: 300, height: 50)
self.view.addSubview(button)
button.setTitle("Test", for: .normal)
button.setTitleColor(UIColor.blue, for: .normal)
button.layer.cornerRadius = 10
button.layer.masksToBounds = true

// 触发离屏渲染
button.backgroundColor = UIColor.green
// 触发离屏渲染
button.setBackgroundImage(UIImage(named: "test.jpg"), for: .normal)

// 3. UIView
let view = UIView(frame: CGRect(x: 50, y: 400 + 50, width: 300, height: 50))
self.view.addSubview(view)
view.backgroundColor = UIColor.red
view.layer.cornerRadius = 10
view.layer.masksToBounds = true

// label如果被渲染,则会触发渲染,如果text为空不会被渲染
let label = UILabel(frame: CGRect(x: 10, y: 10, width: 1, height: 1))
label.text = "1"
view.addSubview(label)
 

出现离屏渲染的地方被标记为黄色

如果设置了cornerRadius+masksToBounds(裁切),并且用于渲染的图层大于1,就会触发离屏渲染,其中如果设置backgroundColor,背景颜色相当于一个单独一个图层,subviews的图层也算,UILabel如果text为空(subviews为空,backgroundColor为空),则不会生成渲染图层

所以设置了cornerRadius+masksToBounds

  • UIImageView设置图片不会触发离屏渲染
  • UIView设置了背景颜色,但不添加subview,不会触发离屏渲染
  • UILabel设置了文字,并且设置了backgroundColor,会触发离屏渲染
  • UIButton只设置文字和背景,会触发离屏渲染

优化圆角问题

基于上面的问题,我们可以有几个优化方向

  1. 避免使用裁切(masksToBounds)操作,如果我们能确保View里面的内容不会溢出,就可以不用masksToBounds
  2. 即使要用到裁切的操作,尽量放到子view里面,不要在上层view使用masksToBounds,因为裁切需要对所有的layer和subview所有图层都进行裁切,这样离屏渲染会需要更大的空间,裁切更多的图层,应该只对必要的view/layer进行裁切
  3. 提前切好需要的圆角,避免渲染的时候再切

其他触发离屏渲染的情况

  • 使用了遮罩的 layer (layer.mask)
  • 需要进行裁剪的 layer (layer.masksToBounds / view.clipsToBounds)
  • 设置了组透明度为 YES,并且透明度不为 1 的layer (layer.allowsGroupOpacity / layer.opacity)
  • 添加了投影的 layer (layer.shadow),但如果设置了shadowPath,则系统已经知道如何绘制阴影了,不会触发离屏渲染
  • 采用了光栅化的 layer (layer.shouldRasterize),光栅化也可以优化离屏渲染问题
  • 绘制了文字的 layer (UILabelCATextLayerCoreText等)

毛玻璃

在iOS系统中,毛玻璃效果应用的非常广泛,从上面分析也可以知道,这个肯定会触发离屏渲染的,图层之间存在依赖,下面是UIBlurEffect的处理过程

在GPU的渲染过程如下图

GPU在渲染完Content之后,会另外开辟一个Off-screen buffer,执行下面步骤,最后再做合并处理,最后再拷贝回On-screen buffer

posted @ 2021-08-17 11:59  小锅炖豆腐  阅读(291)  评论(0编辑  收藏  举报