iOS探索--离屏渲染

3,070 阅读7分钟

iOS 探索系列相关文章 :

iOS 探索 -- alloc、init 与 new 的分析

iOS 探索 -- 内存对齐原理分析

iOS 探索 -- isa 的初始化和指向分析

iOS 探索 -- 类的结构分析(一)

iOS 探索 -- 类的结构分析(二)

iOS 探索 -- 消息查找流程(一)

iOS 探索 -- 消息查找流程(二)

iOS 探索 -- 动态方法决议分析

iOS 探索 -- 消息转发流程分析

iOS 探索 -- 离屏渲染

iOS 探索 -- KVC 原理分析

定义

离屏渲染的大概过程

当我们要在屏幕上显示内容, 至少需要一块与屏幕像素数据量一样大的 frame buffer 来作为数据存储区域 (GPU 渲染结果存储的地方)。但是此时出现了特殊情况导致渲染结果无法直接写入 frame buffer, 而是需要先暂存到另外的区域进行处理, 之后再写入到 frame buffer, 这种情况就称之为 离屏渲染

检测离屏渲染

  • 模拟器 可以通过设置 Debug -> Color Off-screen Rendered 来打开离屏渲染检测
  • 真机 则通过设置 Debug -> View Debugging -> Rendering -> Color Off-screen Rendered 来打开离屏渲染检测
  • 具体情况如上图那样, 图中的颜色呈现 黄色 的区域就是触发了 离屏渲染 的区域

离屏渲染分析

渲染流程分析

接下来我们先一起看 2张 图来看一下整个的 渲染流程:

  1. 通常情况下的渲染通道流程

通常渲染流程 2. Offscreen Render的渲染通道流程

离屏渲染流程 * 在一般情况下 OpenGL 会讲应用提交到 `Render Server` 的动画直接进行渲染显示。(图 1)
  • 但是对于一些较复杂的动画的渲染并不能直接渲染叠加显示, 而是需要根据 Command Buffer 分通道进行渲染之后再组合。(图 2)

  • 对比上面的 2 个流程图, 可以看到后面的渲染需要更多的渲染通道和合并的步骤。

  • 相比之下 Offscreen Render 需要更多的渲染通道, 且多个渲染通道间切换肯定会消耗一定的时间, 当通道达到一定的数量, 对性能必然造成影响

为什么会产生离屏渲染?

正常的情况下, OpenGL 提交一个命令到 Command Buffer , 随后 GPU 就开始渲染, 最后将渲染结果放到 Render Buffer 中。

这里如果想要绘制一个带有圆角并剪切圆角的容器 (maskToBounds 为 YES, 背景色不是透明, 具体可以看下面的例子 1), 就可能触发离屏渲染。

  1. 首先将 layer 的内容裁剪成圆角
  2. 容器的子控件在渲染的过程中, 因为父 layer 是被裁剪过的, 那么也需要被裁剪; 但是这时的 父 layer 已经被渲染完成而子 layer 还在队列中, 没有办法进行统一裁剪, 所以这个过程就没办法实现了

所以系统就不得不去 开辟独立于 frame buffer 的内存, 先把父 layer 以及他的子 layer依次画好, 然后合并到一起进行裁剪, 再把结果放到 frame buffer 中, 这就是为什么需要离屏渲染。

可能不是很好理解, 那么接下来再结合几个案例看一下吧~

注: 以上内容如果有不同观点欢迎指出~

离屏渲染案例

1. cornerRadius + maskToBounds

//UIImageView 设置了图片+背景色;
UIImageView *img1 = [[UIImageView alloc]init];
img1.frame = CGRectMake(100, 320, 100, 100);
img1.backgroundColor = [UIColor blueColor];
[self.view addSubview:img1];
img1.layer.cornerRadius = 50;
img1.layer.masksToBounds = YES;
img1.image = [UIImage imageNamed:@"btn.jpeg"];
//UIImageView 只设置了图片,无背景色;
UIImageView *img2 = [[UIImageView alloc]init];
img2.frame = CGRectMake(100, 480, 100, 100);
[self.view addSubview:img2];
img2.layer.cornerRadius = 50;
img2.layer.masksToBounds = YES;
img2.image = [UIImage imageNamed:@"btn.jpeg"];

执行结果:

  1. (图 1) 设置了 圆角+maskToBounds, 然后设置了背景颜色, 发现产生了离屏渲染, 这个就是我们上面所举的例子
  2. (图 2) 设置了 圆角+maskToBounds, 背景色为透明, 没有发生离屏渲染。这是因为苹果在 iOS 9 之后进行的优化, 假如我们只设置 UIImageViewimage , 并加上圆角+裁剪,是不会产生离屏渲染的。但如果加上了背景色、边框或其他有图像内容的图层,还是会产生离屏渲染。
  3. 所以并不是 圆角+maskToBounds 必定会产生离屏渲染

关于 iOS 9 的优化后

可以理解为,因为只有 单层 内容需要添加圆角和裁切,所以可以不需要用到离屏渲染技术。但如果加上了背景色、边框或其他有图像内容的图层,就会产生为 多层 添加圆角和裁切,所以还是会触发离屏渲染。

2. mask.layer

mask 我们知道mask是应用在layer和其所有子layer的组合之上的,而且可能带有透明度,不得不在离屏渲染中完成。
  1. 系统先计算好mask部分,然后保存到离屏缓冲区
  2. 计算layer部分,计算好之后保存到离屏缓冲区
  3. 对mask和layer进行合并剪裁计算,最后结果提交到FrameBuffer,展示到屏幕上

3. 其它会产生离屏渲染的情况

关于离屏渲染还有很多种情况, 这里不一一赘述了, 感兴趣的可以去网上查找一下, 下面在列举几种会出现的情况:

  1. layer.shouldRasterize 光栅化

  2. edge antialiasing(抗锯齿)

  3. 半透明视图混合时

离屏渲染的优化

1. 针对圆角、阴影效果等的方案

  1. 使用带圆角的图片
  2. 使用贝塞尔曲线进行圆角绘制
  3. 跟服务端讨论进行圆角处理
  4. 当不存在短时间内需要反复多次大量复用的layer时,shouldRasterize设置为NO

2. 可复用时的优化方案

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

最后

以上就是本次的全部内容, 关于离屏渲染的东西可能不是很全面, 欢迎继续补充。另外, 后续可能还会继续补充一下关于 Core Animation 相关的知识, 感谢~

和谐学习, 不急不躁~

参考资料:

1. iOS离屏渲染

2. 关于iOS离屏渲染的深入研究

3.iOS 关于离屏渲染的理解 以及解决方案