上一篇文章,我们了解了iOS渲染架构,以及 GPU 的渲染流程。GPU 的渲染方式有两种。
On-Screen Rendering:当前屏幕渲染,CPU、GPU 不停地将内容渲染完成放入frame buffer
帧缓冲区中,显示屏幕从 frame buffer
中获取内容显示。
Off-Screen Rendering:离屏渲染,先创建离屏渲染帧缓冲区offscreen frame buffer
,然后逐一将内容渲染放入其中,完成后对离屏渲染缓冲区做阴影叠加、裁剪等操作,最后将结果拷贝或切换到帧缓冲区frame buffer
中,显示屏幕从 frame buffer
中获取内容显示。
为什么需要
那么为什么需要离屏渲染帧缓冲区offscreen frame buffer
呢?我们先来了解下“画家算法”。
画家算法,也叫作优先填充,它是三维计算机图形学中处理可见性问题的一种解决方法(三维场景投影到二维平面)。如下图,画家算法首先将场景中的多边形根据深度进行排序,然后按照由远到近的顺序进行描绘,这种方法通常会将不可见的部分覆盖,这样就可以解决可见性问题。
对于有前后依赖的图层(如阴影叠加、裁剪等),通过由远到近的图层叠加算法是无法实现的,我们需要先申请一个临时缓冲区,所有图层按照画家算法,由远到近在临时缓冲区渲染,渲染完成后,再对这个临时缓冲区做最后的全局操作(如阴影叠加、裁剪等),最后再把临时缓冲区拷贝或切换到当前的缓冲区上,交给显示器显示。
总结一下,使用离屏渲染大概是因为以下原因:
-
需要实现特殊的效果,比如说全局叠加、裁剪等等,需要用额外的帧缓冲区
offscreen frame buffer
保存中间状态。 -
出于效率目的,针对不会经常变更的图层,可以缓存到
offscreen frame buffer
,供下次刷新使用。
什么时候出现
首先查看离屏渲染情况,可以打开Xcode
-> Debug
-> View Debuging
-> Rendering
-> Color Offscreen Rendered Yellow
开关来显示。黄色区域表示发生了离屏渲染。不过在实际测试过程中,有出现绿色的区域,猜想可能和Color Hits Green and Misses Red
一样,表示复用了光珊化的离屏渲染缓存。
Masking
最常见的情形就是使用了Masking
蒙版,我们看下官方提供的Masking
渲染流程:
如上图,渲染Masking
蒙版分为3步:
- 渲染
layer
的mask
纹理,流程同Tile Based Rendering
。 - 渲染
layer
的content
纹理,流程同Tile Based Rendering
。 - 合成操作,合并
mask
、content
纹理。
由于需要叠加两层渲染结果,所以在叠加前,需要额外的缓冲区保存渲染结果,也就是触发了离屏渲染。
测试一下
// 1.设置当前 mask ,最后 view 会显示成 mask 的形状,会触发离屏渲染。
let maskLayer = CALayer()
maskLayer.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
maskLayer.contents = UIImage(named: "bc_favored")?.cgImage
self.view.layer.mask = maskLayer
UIVisualEffectView
iOS 8 提供的blur
模糊特效也会引起离屏渲染,我们看下官方提供的blur
渲染流程:
如上图,渲染模糊过程分为4步:
- 渲染
layer
的content
。 - 截获
layer
的content
,进行缩放。 - 对缩放内容进行横向模糊。
- 对缩放内容进行纵向模糊。
- 合成操作,合并所有模糊结果。
blur
模糊效果也会触发离屏渲染,而且需要更多的缓冲区存储渲染结果,更浪费性能。
Rasterization
光栅化,开启光栅化后,会触发离屏渲染,GPU 会强制把图层的渲染结果保存下来,方便下次复用。我们看下官方的描述:
光珊化的本意是为了避免静态内容重绘、复杂view层级重绘带来的影响。使用需要注意以下几点:
- CALayer 的
shouldRasterize
可开启光珊化。 - 如果 layer 不是静态,或者频繁修改(动画中),开启光珊化反而影响效率。
- 不要过度使用,光珊化离屏渲染缓存大小限制为2.5倍屏幕大小。
- 光珊化离屏渲染缓存100ms没有被使用,就会被丢弃。
Group Opactiy
组不透明度,某些情况也会触发离屏渲染,可以通过CALayer
的allowsGroupOpacity
控制。我们看官方的描述:
总结一下,有以下两点:
- 建议关闭
CALayer
的allowsGroupOpacity
属性。iOS 7.0 SDK 以后,allowsGroupOpacity
默认 true 。 - 如果开启了
allowsGroupOpacity
,当 layer 的opacity
小于1.0,且有子 layer 或者背景图,则会触发离屏渲染。
测试一下
// 1. 不会触发离屏渲染
let view = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
view.backgroundColor = UIColor.gray
view.layer.allowsGroupOpacity = true
view.layer.opacity = 0.9
self.view.addSubview(view)
// 2.设置 layer.contents ,触发离屏渲染
view.layer.contents = UIImage(named: "bc_delete")?.cgImage
// 3.addSubview ,触发离屏渲染
let textLab = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40))
textLab.text = "test"
view.addSubview(textLab)
Shadow
投影,和Masking
一样,涉及到多个渲染结果合并,也会启用离屏渲染。来看下官方的解释:
如果单纯设置shadowOffset
,会触发离屏渲染,但是我们可以设置shadowPath
,告诉Core Animation
投影路径,那么系统就知道如何绘制投影了,就不会触发离屏渲染了。
测试一下
// 1. 设置 shadow ,会产生离屏渲染。
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOpacity = 0.3
self.layer.shadowRadius = 3
self.layer.shadowOffset = CGSize.zero
// 2.设置 shadowPath ,告诉 Core Animation 投影路径,则不会出现离屏渲染。
self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: .allCorners
, cornerRadii: CGSize(width: 30, height: 30)).cgPath
圆角
圆角,其实和Masking
一样,也是通过图层叠加实现的。
cornerRadius,只会默认设置
layer backgroundColor
和border
的圆角。 masksToBounds,会对content
裁剪。
单纯的cornerRadius+masksToBounds
不一定会产生离屏渲染,如果这个圆角裁剪操作需要作用多个图层,也就是layer
上有其他图层,那么肯定会发生离屏渲染。比如以下几种情况:
- layer 设置了圆角裁剪,且包含 sub layer,或者有 content,会触发离屏渲染。
- UIImageView 设置了圆角裁剪,同时设置了
backgroundColor
和image
(属于两个图层),也会触发离屏渲染。 - UIButton 设置了圆角裁剪,且设置了
backgroundImage
或者image+backgroundColor
,则会触发离屏渲染。
测试一下
// 1.仅设置 cornerRadius、masksToBounds,不会产生离屏渲染
let view = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
view.layer.cornerRadius = 2
view.layer.masksToBounds = true
self.view.addSubview(view)
// 2.设置 layer.contents ,触发离屏渲染
view.layer.contents = UIImage(named: "bc_delete")?.cgImage
// 3.addSubview ,触发离屏渲染
let textLab = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40))
textLab.text = "test"
view.addSubview(textLab)
// 4.UIImageView同时设置image、backgroundColor,触发离屏渲染
let view = UIImageView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
view.image = UIImage(named: "bc_delete")
view.backgroundColor = UIColor.gray
view.layer.cornerRadius = 20
view.layer.masksToBounds = true
self.view.addSubview(view)
// 5. UIButton 设置了 setBackgroundImage,会触发离屏渲染。
let view = UIButton.init(type: .custom)
view.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
view.layer.cornerRadius = 20
view.layer.masksToBounds = true
// 5.1 会发生离屏渲染
view.backgroundColor = UIColor.gray
view.setImage(UIImage(named: "bc_favored"), for: .normal)
// 5.2 会发生离屏渲染
view.setBackgroundImage(UIImage(named: "bc_favored"), for: .normal)
self.view.addSubview(view)
如何避免
那么如何避免圆角裁剪产生的离屏渲染呢?我们先来看官方描述:
-
不要使用不必要的 mask,可以预处理图片为圆形,通过
Core Graphics
手动绘制圆角。 -
使用中间为圆形透明的白色背景视图覆盖。但这种方式不能解决背景色为图片或渐变色的情况。
-
用
UIBezierPath
贝塞尔曲线绘制闭合带圆角的矩形,再将不带圆角的 layer 渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是layer
的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,稍微麻烦。
总结
最后我们总结一下,常见触发离屏渲染的情况有以下6种:
-
layer
设置了使用了mask
蒙版。 -
layer
设置了圆角裁剪(masksToBounds
+cornerRadius
),且包含 sub layer。 -
layer
设置了组不透明度allowsGroupOpacity
,并且不透明度opacity
小于1。 -
layer
设置了投影shadowXX
。 -
layer
设置了光栅化shouldRasterize
。 -
使用了 blur 模糊效果
UIVisualEffectView
。