iOS渲染 离屏渲染

高级光栅扫描显示系统结构:

image1.png

图像撕裂的原因:

在显示控制器从帧缓冲区读取图像的位图数据并通过光栅逐行扫描在显示器上显示图像时,当这张图像显示到某个位置的时候,GPU将新的一帧图像提交到帧缓冲区并把两个帧缓冲区进行更新后,显示控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂的现象。

图像撕裂的解决办法:

1.垂直同步

垂直同步又称场同步(Vertical synchronization),垂直同步就是加锁,在当前读取的帧数据结束之前,不会读取下一帧的数据。

2.双缓冲区(Double Buffering)

双缓冲区是在帧缓存区中开辟两个缓冲区,一个缓冲区通过显示控制器进行当前帧数据的读取显示,另一个缓冲区进行接收下一帧GPU渲染的图像。两个缓冲区都执行结束,然后再交换缓冲区。
使用垂直同步+双缓冲区,图像撕裂问题解决了,但是有引发了一个新问题掉帧/卡顿。

图像掉帧/卡顿的原因:

image2.png

正常情况下一张图像的显示是CPU+GUP在下一个垂直同步信号来之前全部完成的,所用的时间是16.7ms(1s/60 ≈16.7ms)。如果第二次的垂直同步信号到来时,本次的图像还未处理并显示完,那么第二个垂直同步信号继续处理并显示上次的图像,并丢弃掉新的图像。
即掉帧/卡顿就是重复处理上一次的图像。

iOS 图像渲染原理

参考文档:iOS 图像渲染原理

iOS下的渲染框架:

image3.png

Core Animation 流水线:

image4.png

Render Server 操作分析:

image5.png

事实上,app 本身并不负责渲染,渲染则是由一个独立的进程负责,即 Render Server 进程。

App 通过 IPC 将渲染任务及相关数据提交给 Render Server。Render Server 处理完数据后,再传递至 GPU。最后由 GPU 调用 iOS 的图像设备进行显示。

Core Animation 流水线的详细过程如下:
首先,由 app 处理事件(Handle Events),如:用户的点击操作,在此过程中 app 可能需要更新 视图树,相应地,图层树 也会被更新。
其次,app 通过 CPU 完成对显示内容的计算,如:视图的创建、布局计算、图片解码、文本绘制等。
在完成对显示内容的计算之后,app 对图层进行打包,并在下一次 RunLoop 时将其发送至 Render Server,即完成了一次 Commit Transaction 操作。
Render Server 主要执行 Open GL、Core Graphics 相关程序,并调用 GPU
GPU 则在物理层上完成了对图像的渲染。
最终,GPU 通过 Frame Buffer、视频控制器等相关部件,将图像显示在屏幕上。

在 Core Animation 流水线中,app 调用 Render Server 前的最后一步 Commit Transaction 其实可以细分为 4 个步骤:
1.Layout:Layout 阶段主要进行视图构建,包括:LayoutSubviews 方法的重载,addSubview: 方法填充子视图等。
2.Display:Display 阶段主要进行视图绘制,这里仅仅是设置最要成像的图元数据。重载视图的 drawRect: 方法可以自定义 UIView 的显示,其原理是在 drawRect: 方法内部绘制寄宿图,该过程使用 CPU 和内存。
3.Prepare:Prepare 阶段属于附加步骤,一般处理图像的解码和转换等操作。
4.Commit:Commit 阶段主要将图层进行打包,并将它们发送至 Render Server。该过程会递归执行,因为图层和视图都是以树形结构存在。

什么是离屏渲染

参考文档:关于iOS离屏渲染的深入研究
如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。

善用离屏渲染

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

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

设置圆角(cornerRadius+clipsToBounds)不一定会触发离屏渲染

layer.cornerRadius只会设置 backgroundColor和border的圆角,不会设置content的圆角,除非同时设置了cornerRadius为True或clipsToBounds为True。

常⻅触发离屏渲染的几种情况:

  1. 使用了 mask 的 layer (layer.mask)
  2. 需要进行裁剪的 layer (layer.masksToBounds / view.clipsToBounds)
  3. 设置了组透明度为 YES,并且透明度不为 1 的 layer (layer.allowsGroupOpacity/ layer.opacity)
  4. 添加了投影的 layer (layer.shadow*)
  5. 采用了光栅化的 layer (layer.shouldRasterize)
  6. 绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)

你可能感兴趣的:(iOS渲染 离屏渲染)