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
当选择圆角策略时,只需考虑以下三点:
预合成圆角:
预合成角指使用贝塞尔曲线绘制角的曲线,以在 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 适用于以下两种情况:
参考资料
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等等)保存在一块内存中,这样一来在下一帧仍然可以被复用,而不会再次触发离屏渲染。有几个需要注意的点:
绝大多数情况下,得益于GPU针对图形处理的优化,我们都会倾向于让GPU来完成渲染任务,而给CPU留出足够时间处理各种各样复杂的App逻辑。为此Core Animation做了大量的工作,尽量把渲染工作转换成适合GPU处理的形式(也就是所谓的硬件加速,如layer composition,设置backgroundColor等等)。
但是对于一些情况,如文字(CoreText使用CoreGraphics渲染)和图片(ImageIO)渲染,由于GPU并不擅长做这些工作,不得不先由CPU来处理好以后,再把结果作为texture传给GPU。除此以外,有时候也会遇到GPU实在忙不过来的情况,而CPU相对空闲(GPU瓶颈),这时可以让CPU分担一部分工作,提高整体效率。
参考资料