经常看到说是离屏渲染会影响性能,我们要避免离屏渲染,然后阐述离屏渲染的触发情况有哪些?
既然离屏渲染那么不好,那为什么苹果还在用,那是不是想达到某种展示效果,离屏渲染是无法避免的?接下来就分析下
0x01 渲染方案
GPU
渲染
OpenGL
中,GPU
屏幕渲染有以下两种方式:
-
On-Screen Rendering
:当前屏幕渲染
指的是 GPU 的渲染操作是在当前用于显示的屏幕缓冲区中进行。
正常渲染.png如果要再显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的
FrameBuffer
,作为像素数据存储区域,而这也是GPU
存储渲染结果的地方。正常操作流程如下。
-
Off-Screen Rendering
:离屏渲染指的是
GPU
在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。如果有时因为面临一些限制,无法把渲染结果直接写入
frame buffer
,而是先暂存在另外的内存区域,之后再写入frame buffer
,那么这个过程就称之为离屏渲染。操作流程如下
不管CPU
还是GPU
,只要不能直接在frame buffer
上画,都属于offscreen rendering
。
在Core Animation Advance Techniques
中关于offscreen rendering
的一段说明:
Offscreen rendering is invoked whenever the combination of
layer properties that have been specified mean that the layer cannot be
drawn directly to the screen without pre- compositing.
Offscreen rendering does not necessarily imply software drawing,
but it means that the layer must first be rendered (either by the CPU or GPU) into an offscreen context before being displayed.
相比于当前屏幕渲染,离屏渲染需要付出些代价,主要体现在两个方面:
-
需要创建一个额外的缓冲区。
-
上下文切换离屏渲染的整个过程,需要多次切换上下文环。
先从当前屏幕(
On-Screen
)切换到离屏(Off-Screen
);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。
每次切换上下文,必须刷新其管线和屏障。
CPU
渲染
大多数情况下,屏幕外渲染与屏幕上渲染并不是最重要的问题,因为屏幕外渲染可以与屏幕上的渲染一样快。主要问题是渲染是在硬件还是在软件中完成。
通常,在iOS
中,像素效果和Quartz
/ Core Graphics
绘图不是硬件加速,而大多数其他事情是硬件加速的。
以下内容不是硬件加速的,这意味着它们需要在软件中完成渲染(屏幕外)
1. 在drawRect中完成所有操作。如果您的视图有个drawRect,尽管知识一个空的视图,那么绘图就不会在硬件中完成,并且会降低性能。
2. 将shouldRasterize属性设置为YES的任何层。
3. 带有遮罩或者阴影的任何图层。
4. 文本(任何类型,包括UILabels, CATextLayers, Core Text等)。
5. 您使用CGContext自己绘制的任何图形(屏幕上或屏幕外)
大多数其他事情都是硬件加速的,因此它们要快的多。
与硬件加速绘图相比,以上任何一种绘图类型都比较慢,但是它们并不一定会使您的应用程序变慢。
-
对于不会改变每一帧的大型复杂视图
在软件中对其进行一次绘制(之后对其进行缓存并且无需重新绘制)将比使用硬件在每一帧中重新组合它们产生更好的性能,只不过第一次绘制的比较慢而已。
例如以下策略
第一次在视图上绘制阴影很慢,但是会之后会对其进行缓存,并且仅在视图更改大小或形状时才重新绘制。
栅格化视图或具有自定义
drawRect
的视图也是如此:通常不会在每一帧都重新绘制视图,而是先绘制一次然后缓存,因此除非边界改变或您可以在其上调用setNeedsDisplay
-
对于必须不断重绘的视图
例如(表格单元格,每次滚动都要重绘),软件绘图可能会减慢很多速度。
其实通过CPU
渲染就是俗称的“软件渲染”,而真正的离屏渲染发生在GPU
。
0x02 iOS
渲染架构
在WWDC
的Advanced Graphics and Animations for iOS Apps
(WWDC14 419
,关于UIKit
和Core Animation
基础的session
在早年的WWDC
中比较多)中有这样一张图:
我们可以看到,在Application
这一层中主要是CPU
在操作,而到了Render Server
这一层,CoreAnimation
会将具体操作转换成发送给GPU
的draw calls
(一前是call penGL ES,
现在慢慢转到了Metal
),显然CPU
和GPU
双方处于同一个流水线中,协作完成整个渲染工作。
通常对于每一层layer
, Render Server
会遵循 “画家算法”, 按次序输出到frame buffer
,后一层覆盖前一层,就能得到最终的显示结果(与一般桌面架构不同,在iOS
中,设备主存和GPU
的显存共享物理内存,这样可以省去一些数据传输开销)。
然而有些场景并没有那么简单。作为“画家”的GPU
虽然可以一层一层往画布上进行输出,但是无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分 ——因为这一层之前的若干层layer
像素数据,已经在渲染中被永久覆盖了。
这就意味着,对于每一层layer
,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个中转区域来完成一些更复杂的、多次的修改/裁剪操作。
0x03 当前iOS
"离屏渲染"原因分析
使用group opacity
- 猜想
其实从名字就可以猜到,alpha
并不是分别应用在每一层之上,而是只有到整个layer
树画完之后,再统一加上alpha
,最后和底下其他layer
的像素进行组合。显然也无法通过一次遍历就得到最终结果。
例:
将一对蓝色和红色layer
叠在一起,然后在父layer
上设置opacity=0.5
,并复制一份在旁边作对比。
左边关闭group opacity
,右边保持默认(从iOS7
开始,如果没有显式指定,group opacity
会默认打开),然后打开offscreen rendering
的调试,我们会发现右边的那一组确实是离屏渲染了。
使用shadow
- 猜想
虽然layer
本身是一块矩形区域,但是阴影默认是作用在其中”非透明区域“的,而且需要显示在所有layer
内容的下方,因此根据画家算法必须被渲染在先。
但矛盾在于此时阴影的本体(layer
和其子layer
)都还没有被组合到一起,怎么可能在第一步就画出只有完成最后一步之后才能知道的形状呢?
这样一来又只能另外申请一块内存,把本体内容都先画好,再根据渲染结果的形状,添加阴影到frame buffer
,最后把内容画上去(这只是猜测,实际情况可能更复杂)。
不过如果我们能够预先告诉CoreAnimation
(通过shadowPath
属性)阴影的几何形状,那么阴影当然可以先被独立渲染出来,不需要依赖layer
本体,也就不再需要离屏渲染了。
使用mask
- 猜想
我们知道mask
是应用在layer
和其所有子layer
的组合之上的,而且可能带有透明度,那么其实和group opacity
的原理类似,不得不在离屏渲染中完成。
cornerRadius
与clipsToBounds
同时使用 - 猜想
将一个layer
的内容裁剪成圆角,可能不存在一次遍历就能完成的方法。
容器的子layer
因为父容器有圆角,那么也会需要被裁剪,而这时它们还在渲染队列中排队,尚未被组合到一块画布上,自然也无法统一裁剪。
此时我们就不得不开辟一块独立于frame buffer
的空白内存,先把容器以及其所有子layer
依次画好,然后把四个角“剪”成圆形,再把结果画到frame buffer
中。这就是GPU
的离屏渲染。
0x04 怎么用好离屏渲染
离屏渲染开销 很大,但是我们并不能完全避免,那么我们可以想办法把性能影响降到最低。
-
将渲染结果缓存起来,比如圆角等,下一帧直接使用这个结果,就不需要重新绘制了。
-
calayer
的属性shouldRasterize
一旦设置为
true
,Render Server
就会强制把layer
的渲染结果(包括子layer
, 圆角,阴影,透明度等等)保存在一块内存中,这样一来,下一帧仍然可以使用,而不会出发离屏渲染。
注意:
-
shouldRasterize
主旨是降低性能损失,但是至少会触发一次离屏渲染。如果不是很复杂的layer
(圆角阴影,透明度),打开这个开关反而会增加一次不必要的离屏渲染。 - 离屏渲染缓存空间有上限,最多不超过屏幕总像素的
2.5
倍 - 一旦缓存超过
100ms
没有被使用,会自动丢弃 -
layer
内容如果发生改变,缓存就会失效,需要避免这种情况,Xcode
提供了color Hits Green and Misses Red
,帮助我们查看缓存的使用是否符合预期
0x05 双帧缓冲区与离屏渲染的关系
研究者离屏渲染缓冲区时不知怎么跟双帧缓冲区产生了混淆,为此画了张图做区分:
双帧缓冲.png通过上图可以看出:
- 双帧缓冲区是为了提前渲染一帧数据,解决屏幕展示效率问题。
- 离屏缓冲区,解决的是
GPU
无法把结果直接写到到FrameBuffer
中。
0x06 总结
离屏渲染涉及到很多图形学方面的知识。在实践中不仅要知道解决这些问题的方案,更要探究其内部实现的原理。
本文主要整理网上资源,同时进行自己思维发散。如有小伙伴交流讨论可以留言一起拓展。
参考:
https://stackoverflow.com/questions/6731545/when-does-a-view-or-layer-require-offscreen-rendering
https://zhuanlan.zhihu.com/p/72653360
https://zhuanlan.zhihu.com/p/49829766
https://github.com/seedante/iOS-Note/wiki/Mastering-Offscreen-Render
https://lobste.rs/s/ckm4uw/performance_minded_take_on_ios_design#c_itdkfh
https://blog.csdn.net/xiaoyafang123/article/details/79268157
网友评论