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
,供下次刷新使用。
什么时候出现
Masking
最常见的情形就是使用了Masking
蒙版,我们看下官方提供的Masking
渲染流程:
如上图,渲染Masking
蒙版分为3步:
- 渲染
layer
的mask
纹理,流程同Tile Based Rendering
。 - 渲染
layer
的content
纹理,流程同Tile Based Rendering
。 - 合成操作,合并
mask
、content
纹理。
由于需要叠加两层渲染结果,所以在叠加前,需要额外的缓冲区保存渲染结果,也就是触发了离屏渲染。
UIVisualEffectView
iOS 8 提供的blur
模糊特效也会引起离屏渲染,我们看下官方提供的blur
渲染流程:
如上图,渲染模糊过程分为4步:
- 渲染
layer
的content
。 - 截获
layer
的content
,进行缩放。 - 对缩放内容进行横向模糊。
- 对缩放内容进行纵向模糊。
- 合成操作,合并所有模糊结果。
blur
模糊效果也会触发离屏渲染,而且需要更多的缓冲区存储渲染结果,更浪费性能。
Rasterization
光栅化,开启光栅化后,会触发离屏渲染,GPU 会强制把图层的渲染结果保存下来,方便下次复用。我们看下官方的描述:
光珊化的本意是为了避免静态内容重绘、复杂view层级重绘带来的影响。使用需要注意以下几点:
这里有一个iOS交流圈:891 488 181
- CALayer 的
shouldRasterize
可开启光珊化。 - 如果 layer 不是静态,或者频繁修改(动画中),开启光珊化反而影响效率。
- 不要过度使用,光珊化离屏渲染缓存大小限制为2.5倍屏幕大小。
- 光珊化离屏渲染缓存100ms没有被使用,就会被丢弃。
Group Opactiy
组不透明度,某些情况也会触发离屏渲染,可以通过CALayer
的allowsGroupOpacity
控制。我们看官方的描述:
总结一下,有以下两点:
- 建议关闭
CALayer
的allowsGroupOpacity
属性。 - 如果开启了
allowsGroupOpacity
,当 layer 的opacity
小于1.0,且有子 layer 或者背景图,则会触发离屏渲染。
而在iOS 7.0 SDK以上,allowsGroupOpacity
默认 true,所以不需要的时候,建议关闭。
Shadow
投影,和Masking
一样,涉及到多个渲染结果合并,也会启用离屏渲染。来看下官方的解释:
如果单纯设置shadowOffset
,会触发离屏渲染,但是我们可以设置shadowPath
,告诉Core Animation
投影路径,那么系统就知道如何绘制投影了,就不会触发离屏渲染了。
圆角
通常我们通过layer.cornerRadius
设置圆角,只会默认设置layer backgroundColor
和 border
的圆角,同时结合layer.masksToBounds
对 content
设置圆角。
单纯的cornerRadius+masksToBounds
不一定会产生离屏渲染,如果这个圆角裁剪操作需要作用多个图层,也就是layer
上有其他图层,那么肯定会发生离屏渲染。比如以下两种情况:
- layer 包含 sub layer,或者有 content,会触发离屏渲染。
- 针对
UIImageView
,同时设置了backgroundColor
和image
,会触发离屏渲染。
测试圆角离屏渲染
比如以下代码,运行后,打开Xcode
-> Debug
-> View Debuging
-> Rendering
-> Color Offscreen Rendered Yellow
开关,查看离屏渲染情况:
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)
// 1.设置 layer.contents ,触发离屏渲染
view.layer.contents = UIImage(named: "bc_delete")?.cgImage
// 2.addSubview ,触发离屏渲染
let textLab = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40))
textLab.text = "test"
view.addSubview(textLab)
// 3. 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)
避免圆角离屏渲染
那么如何避免圆角裁剪产生的离屏渲染呢?我们先来看官方描述:
总结一下:
- 不要使用不必要的 mask,可以预处理图片为圆形,通过
Core Graphics
手动绘制圆角。 - 使用中间为圆形透明的白色背景视图覆盖。但这种方式不能解决背景色为图片或渐变色的情况。
- 用
UIBezierPath
贝塞尔曲线绘制闭合带圆角的矩形,再将不带圆角的 layer 渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是layer
的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,稍微麻烦。
总结
最后我们总结一下,常见触发离屏渲染的情况有以下6种:
-
layer
设置了使用了mask
蒙版。 -
layer
设置了圆角裁剪(masksToBounds
+cornerRadius
),且包含 sub layer。 -
layer
设置了组不透明度allowsGroupOpacity
,并且不透明度opacity
小于1。 -
layer
设置了投影shadowXX
。 -
layer
设置了光栅化shouldRasterize
。 - 使用了 blur 模糊效果
UIVisualEffectView
。
另外,我们可以通过打开Xcode
-> Debug
-> View Debuging
-> Rendering
-> Color Offscreen Rendered Yellow
开关,检查离屏渲染情况。
参考链接
博客原文
WWDC14 419
Apple Core Animation Programming Guide
iOS 渲染原理解析
关于iOS离屏渲染的深入研究
文章到这里就结束了,如果你有什么意见和建议欢迎给我留言。你可以加我及时获取最新资料。
作者:老青菜