IOS 离屏渲染

IOS 离屏渲染_第1张图片

application 主要是在CPU操作,而到Render service CoreAnimation会将具体操作转换成发送给GPU的draw Calls ,CPU和GPU处在同一个流水线。

离屏渲染含义: App->offscreen Buffer->Framebuffer

如果要在显示屏上显示内容,至少需哟啊一块与屏幕像素数据量一样大的framebuffer,作为像素数据存储区,这也是GPU存储渲染结果的地方,但是有时候面临一些限制,无法直接将渲染结果写入Framebuffer,而是先暂存另外存储区域,之后再写入offscreen buffer。

CPU“离屏渲染”:

对于类似这种“新开一块CGContext来画图“的操作,有很多文章和视频也称之为“离屏渲染”(因为像素数据是暂时存入了CGContext,而不是直接到了frame buffer)。进一步来说,其实所有CPU进行的光栅化操作(如文字渲染、图片解码),都无法直接绘制到由GPU掌管的frame buffer,只能暂时先放在另一块内存之中,说起来都属于“离屏渲染”。

自然我们会认为,因为CPU不擅长做这件事,所以我们需要尽量避免它,就误以为这就是需要避免离屏渲染的原因。但是根据苹果工程师的说法,CPU渲染并非真正意义上的离屏渲染。另一个证据是,如果你的view实现了drawRect,此时打开Xcode调试的“Color offscreen rendered yellow”开关,你会发现这片区域不会被标记为黄色,说明Xcode并不认为这属于离屏渲染。

其实通过CPU渲染就是俗称的“软件渲染”,而真正的离屏渲染发生在GPU

GPU离屏渲染:

主要的渲染操作都是由CoreAnimation的Render Server模块,通过调用显卡驱动所提供的OpenGL/Metal接口来执行的。通常对于每一层layer,render server 会遵循“画家算法”,按次序输入到frame buffer,后一层覆盖前一层,最终得到显示结果。但是些场景比较特殊,画家算法CGPU虽然可以一层一层往画布上进行输出,但是无法再某一层渲染完成后,再回过头来擦除/改变其中某部分,因为在这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖,意味着对于每一层layer,要么能够找到一种通过单次便利就能完成的渲染算法,哟啊么久不得不开另一块内存,借助这个临时中站区域来完成更加复杂的,多次的修改,裁剪操作。

常见离屏渲染场景分析

001 cornerRadius+clipsToBounds

当选择圆角策略时,只需考虑以下三点:

  1. 圆角下(movement underneath the corner)是否有滑动。
  2. 是否有穿过圆角滑动(movement through the corner)。
  3. 四个圆角是否处于同一个 node 上,有没有与其他 node 相交。


预合成圆角:

预合成角指使用贝塞尔曲线绘制角的曲线,以在 CGContext / UIGraphicsContext 中剪切内容。在这种情况下,拐角将成为图像本身的一部分,并被同步到CALayer中,有两种类型的预合成角:

最佳方案为使用预合成的不透明角(precomposited opaque corner),这是最高效的方法,可实现零 alpha 混合(尽管这比避免触发屏外渲染的重要性小很多),但其不够灵活。如果圆角的对象需要移动,则后面的背景将需要为纯色。使用 Texture 或图片背景会很棘手,推荐使用预合成的 alpha 角。

第二种涉及贝塞尔曲线的角是预合成 alpha 角(precomposited alpha corner),此方法非常灵活,是最常用的方法之一。其会增加在这个内容上进行 alpha 混合的成本,并且 alpha 通道不透明会增加25%内存消耗,但这些消耗对于现代设备来说很小,与cornerRadius触发的离屏渲染不在同一数量级。

预合成角要求角必须在同一 node,且不与 subnode 相交。不满足任一条件,则需使用裁剪圆角。

裁剪圆角 Clip Corner

Clip corner 通过向四个角放置四个不透明内容实现圆角。这种方法灵活、性能高。四个单独 layer 对于 CPU 来说性能消耗很少。

Clip Corner 适用于以下两种情况:

  • 四个角在不同 node,或与 subnode 相交。
  • 圆角只在 node 顶部。

参考资料

002 shadow,其原因在于,虽然layer本身是一块矩形区域,但是阴影默认是作用在其中”非透明区域“的,而且需要显示在所有layer内容的下方,因此根据画家算法必须被渲染在先。但矛盾在于此时阴影的本体(layer和其子layer)都还没有被组合到一起,怎么可能在第一步就画出只有完成最后一步之后才能知道的形状呢?这样一来又只能另外申请一块内存,把本体内容都先画好,再根据渲染结果的形状,添加阴影到frame buffer,最后把内容画上去(这只是我的猜测,实际情况可能更复杂)。不过如果我们能够预先告诉CoreAnimation(通过shadowPath属性)阴影的几何形状,那么阴影当然可以先被独立渲染出来,不需要依赖layer本体,也就不再需要离屏渲染了

003 group opacity,其实从名字就可以猜到,alpha并不是分别应用在每一层之上,而是只有到整个layer树画完之后,再统一加上alpha,最后和底下其他layer的像素进行组合。显然也无法通过一次遍历就得到最终结果。将一对蓝色和红色layer叠在一起,然后在父layer上设置opacity=0.5,并复制一份在旁边作对比。左边关闭group opacity,右边保持默认(从iOS7开始,如果没有显式指定,group opacity会默认打开),然后打开offscreen rendering的调试,我们会发现右边的那一组确实是离屏渲染了。

004 mask,我们知道mask是应用在layer和其所有子layer的组合之上的,而且可能带有透明度,那么其实和group opacity的原理类似,不得不在离屏渲染中完成。

尽管离屏渲染开销很大,但是当我们无法避免它的时候,可以想办法把性能影响降到最低。优化思路也很简单:既然已经花了不少精力把图片裁出了圆角,如果我能把结果缓存下来,那么下一帧渲染就可以复用这个成果,不需要再重新画一遍了。

CALayer为这个方案提供了对应的解法:shouldRasterize。一旦被设置为true,Render Server就会强制把layer的渲染结果(包括其子layer,以及圆角、阴影、group opacity等等)保存在一块内存中,这样一来在下一帧仍然可以被复用,而不会再次触发离屏渲染。有几个需要注意的点:

  • shouldRasterize的主旨在于降低性能损失,但总是至少会触发一次离屏渲染。如果你的layer本来并不复杂,也没有圆角阴影等等,打开这个开关反而会增加一次不必要的离屏渲染
  • 离屏渲染缓存有空间上限,最多不超过屏幕总像素的2.5倍大小
  • 一旦缓存超过100ms没有被使用,会自动被丢弃
  • layer的内容(包括子layer)必须是静态的,因为一旦发生变化(如resize,动画),之前辛苦处理得到的缓存就失效了。如果这件事频繁发生,我们就又回到了“每一帧都需要离屏渲染”的情景,而这正是开发者需要极力避免的。针对这种情况,Xcode提供了“Color Hits Green and Misses Red”的选项,帮助我们查看缓存的使用是否符合预期
  • 其实除了解决多次离屏渲染的开销,shouldRasterize在另一个场景中也可以使用:如果layer的子结构非常复杂,渲染一次所需时间较长,同样可以打开这个开关,把layer绘制到一块缓存,然后在接下来复用这个结果,这样就不需要每次都重新绘制整个layer树了

IOS 离屏渲染_第2张图片

绝大多数情况下,得益于GPU针对图形处理的优化,我们都会倾向于让GPU来完成渲染任务,而给CPU留出足够时间处理各种各样复杂的App逻辑。为此Core Animation做了大量的工作,尽量把渲染工作转换成适合GPU处理的形式(也就是所谓的硬件加速,如layer composition,设置backgroundColor等等)。

但是对于一些情况,如文字(CoreText使用CoreGraphics渲染)和图片(ImageIO)渲染,由于GPU并不擅长做这些工作,不得不先由CPU来处理好以后,再把结果作为texture传给GPU。除此以外,有时候也会遇到GPU实在忙不过来的情况,而CPU相对空闲(GPU瓶颈),这时可以让CPU分担一部分工作,提高整体效率。

  • 渲染不是CPU的强项,调用CoreGraphics会消耗其相当一部分计算时间,并且我们也不愿意因此阻塞用户操作,因此一般来说CPU渲染都在后台线程完成(这也是AsyncDisplayKit的主要思想),然后再回到主线程上,把渲染结果传回CoreAnimation。这样一来,多线程间数据同步会增加一定的复杂度
  • 同样因为CPU渲染速度不够快,因此只适合渲染静态的元素,如文字、图片(想象一下没有硬件加速的视频解码,性能惨不忍睹)
  • 作为渲染结果的bitmap数据量较大(形式上一般为解码后的UIImage),消耗内存较多,所以应该在使用完及时释放,并在需要的时候重新生成,否则很容易导致OOM
  • 如果你选择使用CPU来做渲染,那么就没有理由再触发GPU的离屏渲染了,否则会同时存在两块内容相同的内存,而且CPU和GPU都会比较辛苦
  • 一定要使用Instruments的不同工具来测试性能,而不是仅凭猜测来做决定
  • 对于图片的圆角,统一采用“precomposite”的策略,也就是不经由容器来做剪切,而是预先使用CoreGraphics为图片裁剪圆角
  • 对于视频的圆角,由于实时剪切非常消耗性能,我们会创建四个白色弧形的layer盖住四个角,从视觉上制造圆角的效果
  • 对于view的圆形边框,如果没有backgroundColor,可以放心使用cornerRadius来做
  • 对于所有的阴影,使用shadowPath来规避离屏渲染
  • 对于特殊形状的view,使用layer mask并打开shouldRasterize来对渲染结果进行缓存
  • 对于模糊效果,不采用系统提供的UIVisualEffect,而是另外实现模糊效果(CIGaussianBlur),并手动管理渲染结果

 

参考资料

 

 

你可能感兴趣的:(个人OC知识梳理)