iOS系统中离屏渲染利与弊
阅读需要约8分钟
前言
性能的优化相信是每一个APP工程师所追求的,而离屏渲染就是一个绕不开的知识点。现在提出几个问题帮助大家更快的理解:
- 什么情况下会触发离屏渲染,为什么?
- 利、弊都有哪些?
- 离屏渲染是如何发生的
先通过模拟器来看看是情况下会发生离屏渲染。
打开模拟器 - Debug - Color Off-screen Rendered开关
提起离屏渲染,下意识的就是会想到cornerRadius
这个属性,设置圆角就会导致离屏渲染,事实是这样吗?
let view1 = UIView(frame: rect)
view1.backgroundColor = UIColor.red
view1.layer.cornerRadius = 50
view1.layer.borderWidth = 2
view.addSubview(view1)
没有触发离屏渲染
我们先来看看苹果官方文档对于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时,只为layer的backgroundColor
和border
设置圆角;而不会对layer的contents
设置圆角,除非同时设置了layer.masksToBounds
为true或者UIView的clipsToBounds
属性。那我们设置这两个属性看一下。
let view1 = UIView(frame: rect)
view1.backgroundColor = UIColor.red
view1.layer.cornerRadius = 50
view1.layer.borderWidth = 2
view1.layer.masksToBounds = true
view1.clipsToBounds = true
view.addSubview(view1)
事实上还是没有触发离屏渲染,这就很奇怪了。
这时候就需要了解一下几个属性的关系了
发现其实图中3个属性,对应了3个图层。我们看到的图像其实是这3个图层的合并,这面会涉及到一个画家算法
,下一节会说到。原来contents是单独的个图层,那我们设置它看一下。
let view1 = UIView(frame: rect)
view1.backgroundColor = UIColor.red
view1.layer.cornerRadius = 50
view1.layer.borderWidth = 2
view1.layer.contents = UIImage(named: s)?.cgImage
view1.layer.masksToBounds = true
view1.clipsToBounds = true
view.addSubview(view1)
在当同时设置了**backgroundColor|borderWidth
**,**cornerRadius
**,**contents
**,**masksToBounds
**这四个属性时,导致了离屏渲染。
我们设置了content后触发了离屏渲染,如果我们只设置content看看会不会触发呢?
代码语言:javascript复制 let view1 = UIView(frame: rect)
view1.layer.cornerRadius = 50
view1.layer.contents = UIImage(named: s)?.cgImage
view1.layer.masksToBounds = true
view1.clipsToBounds = true
view.addSubview(view1)
在其他设置都没有变的情况下,只设置了content并没有触发离屏渲染。有没有隐约感觉到一点触发规律:
- 设置了
cornerRadius
- 同时设置了
backgroundColor或者borderWidth
和content
- 设置了
masksToBounds
或者clipsToBounds
**
可是为什么呢?想要搞清楚就需要了解画家算法
。
画家算法
所谓画家算法
是计算机将多个图层由远到近进行绘制的一种算法。先的图层会被后的图层所覆盖。
这里接涉及到计算机渲染的原理,其中非常重要的一个点就是:图层被渲染到**画布
**上之后,当前图层就会被永久销毁,所以面对多个图层时从远到近绘制,保证了可视范围内容的完整,最后保存到帧缓存区等待读取。
- 离屏渲染产生的原理
需要在额外的内存中完成多图层组合绘制工作
GPU中的离屏渲染
现在我对上图中增加一个圆角,而上图是由3个图层组成的,且图层渲染到画布后就会被销毁,导致GPU没办法一次性拿到所有图层来进行圆角切割.
所以现在就需要创建一块空间,将3个图层都保存起来后完成圆角切割。而这个单独的空间就叫做offSet-buffer
,离屏渲染就这样产生了,当然这个图层绘制一般是由GPU完成的,也有些特殊情况下CPU也会参与绘制。
CPU中的“离屏渲染”
- 在CoreAnimation 渲染流程中
Display
流程的视图层绘制中提过,如果开启drawRect:
方法就会触发CPU的“离屏渲染”,该方法里的所有代码都是在CPU中进行执行,知道完成bitmap,转存到帧缓存区中。但是根据苹果工程师的说法,CPU的渲染并不是真正意义的离屏渲染,当然通过Xcode调试也能看出来,该区域并没有被标记为黄色,说明Xcode也认为这不属于离屏渲染。
label没有触发xcode离屏渲染,所以推测绘制文字的 layer (UILabel, CATextLayer, Core Text 等)是使用CPU来进行的渲染,说一下我这样的猜测的理由:文字渲染更多涉及逻辑计算所以CPU更加适合做这件事。
- 常见离屏渲染场景分析
1. cornerRadius clipsToBounds
同时打开backgroundColor|borderWidth
,cornerRadius
,contents
,masksToBounds
这个情况前面已经分析过了,属性的打开会导致多个图层的裁剪,所以必须在offset-buffer中完成全图图层裁剪后才可以放入帧缓存区中。当然还有其他方法设置圆角但不会触发离屏渲染UIBezierPath
。
UIBezierPath会涉及到CoreGraphics
,在渲染流程
中负责图层的绘制。可知使用了UIBezierPath在每一个单图层绘制的计算中就已经处理了每个图层的圆角,这时画在画布
上的图层就已经是圆角了,估避免了离屏渲染。
2. shadow
开启shadow后会增加一个额外的图层,这个图层是在最先被绘制的,可是这时并不知道content的大小,所以还是没法分开绘制,需要offSet-buffer
的支持。
也可以使用shadowPath
提前告知阴影路劲就可以避免离屏渲染。
3. group opacity(组透明度)
这个很好理解,多个图层都带着透明度,在重叠位置会造成颜色的混合。重叠后的颜色需要计算,而上一层已经被销毁
了,计算机并不知道其颜色所以无法计算。这时就需要把所有图层都存到offset-Buffer,不可避免的触发了离屏渲染。
4. mask
增加mask涉及到多图层的重叠,而且有些图层是有透明度的,原理和group opacity类似。
5. shouldRasterize(光栅化)
及时离屏渲染消耗很大,但是面对复杂图层,好不容易绘制好了为什么不想办法复用
它呢?这时苹果大大就提供了shouldRasterize
这个属性,打开这个属性造成离屏渲染,但是渲染好的内容会被缓存起来,一定条件下就可以复用。如果合理使用这个属性就可以将性能消耗降到最低。
满足这几个条件就可以使用shouldRasterize:
- layer的内容(包括子layer)必须是静态的,因为一旦发生变化,之前辛苦处理得到的缓存就失效了。如果这件事频繁发生,我们就又回到了“每一帧都需要离屏渲染”的情景,而这正是开发者需要极力避免的。针对这种情况,Xcode提供了“Color Hits Green and Misses Red”的选项,帮助我们查看缓存的使用是否符合预期
- 由于系统提供的offset-buffer的空间是有限的:
屏幕尺寸的2.5倍
。所以无法长时间占用,系统只提供了100ms
,如果100ms
没被使用依旧会被释放。
6. 文字以及drawRect
CPU的'离屏渲染'时已经说明,不在赘述。
后续
最初的两个问题在文中已经有明确的答案,耐心阅读可能会有新发现。
推荐阅读:
极客技术团队-关于iOS离屏渲染的深入研究