离屏烘托的定义

在显现屏上显现内容,需求一块与屏幕像素数据量一样大的frame buffer来作为像素数据存储区域,而这也是GPU存储烘托成果的地方。假如有时因为面临一些约束,无法把烘托成果直接写入frame buffer,而是先暂存在别的的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏烘托。

iOS开发中的离屏渲染

CPU”离屏烘托“

假如咱们在UIView中完成了drawRect办法,就算它的函数体内部实际没有代码,系统也会为这个view请求一块内存区域,等待CoreGraphics或许的绘画操作。

关于类似这种“新开一块CGContext来画图“的操作,有许多文章和视频也称之为“离屏烘托”(因为像素数据是暂时存入了CGContext,而不是直接到了frame buffer)。进一步来说,其实全部CPU进行的光栅化操作(如文字烘托、图片解码),都无法直接制作到由GPU掌管的frame buffer,只能暂时先放在另一块内存之中,说起来都属于“离屏烘托”。 因为CPU不拿手做烘托,所以咱们需求尽量防止它,就误以为这便是需求防止离屏烘托的原因。可是根据苹果工程师的说法,CPU烘托并非真正意义上的离屏烘托。另一个证据是,假如你的view完成了drawRect,此刻翻开Xcode调试的“Color offscreen rendered yellow”开关,你会发现这片区域不会被标记为黄色,说明Xcode并不认为这属于离屏烘托。

其实通过CPU烘托便是俗称的“软件烘托”,而真正的离屏烘托产生在GPU。

GPU离屏烘托

在iOS的烘托过程中,首要的烘托操作都是由CoreAnimation的Render Server模块通过调用显卡驱动所供给的OpenGL/Metal接口来执行的。通常关于每一层layer,Render Server会遵从“画家算法”,按次序输出到frame buffer,后一层掩盖前一层,就能得到终究的显现成果;

iOS开发中的离屏渲染

然而有些场景并没有那么简单。作为“画家”的GPU尽管能够一层一层往画布上进行输出,可是无法在某一层烘托完结之后,再回过头来改变其间的某个部分——因为在这一层之前的若干层layer像素数据,现已在烘托中被永久掩盖了。这就意味着,关于每一层layer,要么能找到一种通过单次遍历就能完结烘托的算法,要么就不得不另开一块内存,凭借这个暂时中转区域来完结一些更杂乱的、多次的修改/取舍操作。

假如要制作一个带有圆角并剪切圆角以外内容的容器,就会触发离屏烘托。我的猜想是(假如读者中有图形学专家希望能纠正):

  • 将一个layer的内容裁剪成圆角,或许不存在一次遍历就能完结的办法。
  • 容器的子layer因为父容器有圆角,那么也会需求被裁剪,而这时它们还在烘托队列中排队,尚未被组合到一块画布上,自然也无法一致裁剪

此刻咱们就不得不开辟一块独立于frame buffer的空白内存,先把容器以及其全部子layer依次画好,然后把四个角“剪”成圆形,再把成果画到frame buffer中。这便是GPU的离屏烘托。

常见离屏烘托场景分析

1,cornerRadius+clipsToBounds,原因就如同上面说到的,不得已只能另开一块内存来操作。而假如仅仅设置cornerRadius(如不需求剪切内容,只需求一个带圆角的边框),或许仅仅需求裁掉矩形区域以外的内容(尽管也是剪切,可是略微想一下就能够发现,关于纯矩形而言,完成这个算法似乎并不需求另开内存),并不会触发离屏烘托。关于剪切圆角的性能优化,根据场景不同有几个计划可供选择,十分引荐阅览AsyncDisplayKit中的一篇文档。

2,shadow,其原因在于,尽管layer本身是一块矩形区域,可是暗影默许是作用在其间”非通明区域“的,并且需求显现在全部layer内容的下方,因而根据画家算法有必要被烘托在先。但对立在于此刻暗影的本体(layer和其子layer)都还没有被组合到一同,怎样或许在第一步就画出只有完结终究一步之后才干知道的形状呢?这样一来又只能别的请求一块内存,把本体内容都先画好,再根据烘托成果的形状,添加暗影到frame buffer,终究把内容画上去(这仅仅我的猜想,实际状况或许更杂乱)。不过假如咱们能够预先告诉CoreAnimation(通过shadowPath属性)暗影的几何形状,那么暗影当然能够先被独立烘托出来,不需求依赖layer本体,也就不再需求离屏烘托了。

iOS开发中的离屏渲染

3,group opacity,其实从名字就能够猜到,alpha并不是别离应用在每一层之上,而是只有到整个layer树画完之后,再一致加上alpha,终究和底下其他layer的像素进行组合。显然也无法通过一次遍历就得到终究成果。将一对蓝色和红色layer叠在一同,然后在父layer上设置opacity=0.5,并复制一份在旁边作比照。左面关闭group opacity,右边坚持默许(从iOS7开端,假如没有显式指定,group opacity会默许翻开),然后翻开offscreen rendering的调试,咱们会发现右边的那一组确实是离屏烘托了。

iOS开发中的离屏渲染

4,mask,咱们知道mask是应用在layer和其全部子layer的组合之上的,并且或许带有通明度,那么其实和group opacity的原理类似,不得不在离屏烘托中完结。

GPU离屏烘托的性能影响

GPU的操作是高度流水线化的。原本全部计算作业都在有条不紊地正在向frame buffer输出,此刻忽然收到指令,需求输出到另一块内存,那么流水线中正在进行的全部都不得不被丢掉,切换到只能服务于咱们当时的“切圆角”操作。等到完结今后再次清空,再回到向frame buffer输出的正常流程。

在tableView或许collectionView中,翻滚的每一帧改变都会触发每个cell的从头制作,因而一旦存在离屏烘托,上面说到的上下文切换就会每秒产生60次,并且很或许每一帧有几十张的图片要求这么做,关于GPU的性能冲击可想而知(GPU十分拿手大规模并行计算,可是我想频频的上下文切换显然不在其规划考量之中)

善用离屏烘托

尽管离屏烘托开支很大,可是当咱们无法防止它的时分,能够想办法把性能影响降到最低。优化思路也很简单:既然现已花了不少精力把图片裁出了圆角,假如我能把成果缓存下来,那么下一帧烘托就能够复用这个成果,不需求再从头画一遍了。

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树了

什么时分需求CPU烘托

烘托性能的调优,其实始终是在做一件事:平衡CPU和GPU的负载,让他们尽量做各自最拿手的作业。

绝大多数状况下,得益于GPU针对图形处理的优化,咱们都会倾向于让GPU来完结烘托使命,而给CPU留出足够时间处理各种各样杂乱的App逻辑。为此Core Animation做了许多的作业,尽量把烘托作业转换成适合GPU处理的形式(也便是所谓的硬件加速,如layer composition,设置backgroundColor等等)。

可是关于一些状况,如文字(CoreText运用CoreGraphics烘托)和图片(ImageIO)烘托,因为GPU并不拿手做这些作业,不得不先由CPU来处理好今后,再把成果作为texture传给GPU。除此以外,有时分也会遇到GPU真实忙不过来的状况,而CPU相对空闲(GPU瓶颈),这时能够让CPU分管一部分作业,进步全体功率。

一个典型的例子是,咱们经常会运用CoreGraphics给图片加上圆角(将图片中圆角以外的部分烘托成通明)。整个过程全部是由CPU完结的。这样一来既然咱们现已得到了想要的效果,就不需求再别的给图片容器设置cornerRadius。另一个优点是,咱们能够灵敏地控制裁剪和缓存的机遇,巧妙避开CPU和GPU最繁忙的时段,到达滑润性能动摇的意图。

这里有几个需求留意的点:

  • 烘托不是CPU的强项,调用CoreGraphics会耗费其适当一部分计算时间,并且咱们也不愿意因而阻塞用户操作,因而一般来说CPU烘托都在后台线程完结(这也是AsyncDisplayKit的首要思想),然后再回到主线程上,把烘托成果传回CoreAnimation。这样一来,多线程间数据同步会添加一定的杂乱度
  • 相同因为CPU烘托速度不够快,因而只适合烘托静态的元素,如文字、图片
  • 作为烘托成果的bitmap数据量较大(形式上一般为解码后的UIImage),耗费内存较多,所以应该在运用完及时释放,并在需求的时分从头生成,否则很简单导致OOM
  • 假如你选择运用CPU来做烘托,那么就没有理由再触发GPU的离屏烘托了,否则会一起存在两块内容相同的内存,并且CPU和GPU都会比较辛苦
  • 一定要运用Instruments的不同工具来测验性能,而不是仅凭猜想来做决议

立刻的优化

因为在iOS10之后,系统的规划风格慢慢从扁平化转变成圆角卡片,立刻的规划风格也随之产生改变,加入了许多圆角与暗影效果,假如在处理上稍有不慎,就很简单触发离屏烘托。为此咱们采取了以下一些办法:

  • 立刻许多应用AsyncDisplayKit(Texture)作为首要烘托框架,关于文字和图片的异步烘托操作交由框架来处理。关于这方面能够看我之前的一些介绍
  • 关于图片的圆角,一致选用“precomposite”的策略,也便是不经由容器来做剪切,而是预先运用CoreGraphics为图片裁剪圆角
  • 关于视频的圆角,因为实时剪切十分耗费性能,咱们会创建四个白色弧形的layer盖住四个角,从视觉上制造圆角的效果
  • 关于view的圆形边框,假如没有backgroundColor,能够放心运用cornerRadius来做
  • 关于全部的暗影,运用shadowPath来躲避离屏烘托
  • 关于特别形状的view,运用layer mask并翻开shouldRasterize来对烘托成果进行缓存
  • 关于含糊效果,不选用系统供给的UIVisualEffect,而是别的完成含糊效果(CIGaussianBlur),并手动办理烘托成果
  • 总结:

离屏烘托牵涉了许多Core Animation、GPU和图形学等等方面的常识,在实践中也十分考验一个工程师排查问题的基本功、经历和判断能力——假如在不恰当的时分翻开了shouldRasterize,只会弄巧成拙。