美文网首页
离屏渲染引发的反思

离屏渲染引发的反思

作者: 哦小小树 | 来源:发表于2020-05-10 16:48 被阅读0次

    经常看到说是离屏渲染会影响性能,我们要避免离屏渲染,然后阐述离屏渲染的触发情况有哪些?

    既然离屏渲染那么不好,那为什么苹果还在用,那是不是想达到某种展示效果,离屏渲染是无法避免的?接下来就分析下


    0x01 渲染方案

    GPU 渲染

    OpenGL 中,GPU 屏幕渲染有以下两种方式:

    • On-Screen Rendering :当前屏幕渲染
      指的是 GPU 的渲染操作是在当前用于显示的屏幕缓冲区中进行。

    如果要再显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的FrameBuffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。正常操作流程如下。

    正常渲染.png
    • Off-Screen Rendering:离屏渲染

      指的是 GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

      如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程就称之为离屏渲染。操作流程如下

    离屏渲染.png

    不管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 渲染架构

    WWDCAdvanced Graphics and Animations for iOS AppsWWDC14 419,关于UIKitCore Animation基础的session在早年的WWDC中比较多)中有这样一张图:

    渲染架构.jpg

    我们可以看到,在Application这一层中主要是CPU在操作,而到了Render Server这一层,CoreAnimation会将具体操作转换成发送给GPUdraw calls(一前是call penGL ES, 现在慢慢转到了Metal),显然CPUGPU双方处于同一个流水线中,协作完成整个渲染工作。

    通常对于每一层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的原理类似,不得不在离屏渲染中完成。

    cornerRadiusclipsToBounds同时使用 - 猜想

    将一个layer的内容裁剪成圆角,可能不存在一次遍历就能完成的方法。

    容器的子layer因为父容器有圆角,那么也会需要被裁剪,而这时它们还在渲染队列中排队,尚未被组合到一块画布上,自然也无法统一裁剪。

    此时我们就不得不开辟一块独立于frame buffer的空白内存,先把容器以及其所有子layer依次画好,然后把四个角“剪”成圆形,再把结果画到frame buffer中。这就是GPU的离屏渲染。


    0x04 怎么用好离屏渲染

    离屏渲染开销 很大,但是我们并不能完全避免,那么我们可以想办法把性能影响降到最低。

    • 将渲染结果缓存起来,比如圆角等,下一帧直接使用这个结果,就不需要重新绘制了。

    • calayer的属性shouldRasterize

      一旦设置为true, Render Server就会强制把layer的渲染结果(包括子layer, 圆角,阴影,透明度等等)保存在一块内存中,这样一来,下一帧仍然可以使用,而不会出发离屏渲染。

    注意:

    1. shouldRasterize主旨是降低性能损失,但是至少会触发一次离屏渲染。如果不是很复杂的layer(圆角阴影,透明度),打开这个开关反而会增加一次不必要的离屏渲染。
    2. 离屏渲染缓存空间有上限,最多不超过屏幕总像素的2.5
    3. 一旦缓存超过100ms没有被使用,会自动丢弃
    4. 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://stackoverflow.com/questions/13158796/what-triggers-offscreen-rendering-blending-and-layoutsubviews-in-ios

    https://lobste.rs/s/ckm4uw/performance_minded_take_on_ios_design#c_itdkfh

    https://blog.csdn.net/xiaoyafang123/article/details/79268157

    相关文章

      网友评论

          本文标题:离屏渲染引发的反思

          本文链接:https://www.haomeiwen.com/subject/myvjnhtx.html