iOS 真实的离屏渲染
我们都知道在iOS中离屏渲染会影响性能,它是在GPU的帧缓冲区(framebuff)之外额外的开辟了一段内存空间来进行渲染的,那么它为什么需要这样做呢?
离屏渲染具体过程
简单理解,通常的渲染流程可简化为下面这个样子。
image-20200709095311191App 通过 CPU 和 GPU 的合作,不停地将内容渲染完成放入 Framebuffer 帧缓冲器中,而显示屏幕不断地从 Framebuffer 中获取内容,显示实时的内容。
而离屏渲染的流程是这样的:
image-20200709095341125与普通情况下 GPU 直接将渲染好的内容放入 Framebuffer 中不同,需要先额外创建离屏渲染缓冲区 Offscreen Buffer,将提前渲染好的内容放入其中,等到合适的时机再将 Offscreen Buffer 中的内容进一步叠加、渲染,完成后将结果切换到 Framebuffer 中。
离屏渲染的效率问题
从上面的流程来看,离屏渲染时由于 App 需要提前对部分内容进行额外的渲染并保存到 Offscreen Buffer,以及需要在必要时刻对 Offscreen Buffer 和 Framebuffer 进行内容切换,所以会需要更长的处理时间(实际上这两步关于 buffer 的切换代价都非常大)。
并且 Offscreen Buffer 本身就需要额外的空间,大量的离屏渲染可能早能内存的过大压力。与此同时,Offscreen Buffer 的总大小也有限,不能超过屏幕总像素的 2.5 倍。
可见离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,很容易造成掉帧的问题。所以大部分情况下,我们都应该尽量避免离屏渲染。
为什么要使用离屏渲染
既然离屏渲染开销巨大,那么我们为什么还要使用离屏渲染呢?
- 一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。
- 处于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。
自动触发
mask
对于第一种情况,也就是不得不使用离屏渲染的情况,一般都是系统自动触发的,比如阴影、圆角等等。
最常见的情形之一就是:使用了 mask 蒙版。
image-20200709095841419由于最终的结果是由两个图层叠加而成,所以需要额外的内存空间来存储这两个图层的中间状态。在合适的时机来合成为最终的图层。因此系统会自动触发离屏渲染。
UIBlurEffectView
再比如iOS 8 开始提供的模糊特效 UIBlurEffectView:
image-20200709102104888 image-20200709102141651模糊效果主要分为以下几个步骤:
- 渲染需要模糊的内容。
- 对已经渲染的内容进行缩放。
- 对渲染的内容进行垂直模糊。
- 对渲染的内容进行水平模糊
- 在将前面的四个步骤的结果进行合成,组合成最终的结果。
- 在将合成后的结果渲染到framebuff中,等待下一次的屏幕渲染。显示出最终的结果。
而这个过程,需要保存pass1-到pass5的一个结果状态,直到pass5最终被提交到framebuff中,这时之前的显存空间,会被立即释放掉。
手动触发
shouldRasterize 光栅化
When the value of this property is
YES
, the layer is rendered as a bitmap in its local coordinate space and then composited to the destination with any other content.
开启光栅化后,会触发离屏渲染,Render Server 会强制将 CALayer 的渲染位图结果 bitmap 保存下来,这样下次再需要渲染时就可以直接复用,从而提高效率。
而保存的 bitmap 包含 layer 的 subLayer、圆角、阴影、组透明度 group opacity 等,所以如果 layer 的构成包含上述几种元素,结构复杂且需要反复利用,那么就可以考虑打开光栅化。
圆角、阴影、组透明度等会由系统自动触发离屏渲染,那么打开光栅化可以节约第二次及以后的渲染时间。而多层 subLayer 的情况由于不会自动触发离屏渲染,所以相比之下会多花费第一次离屏渲染的时间,但是可以节约后续的重复渲染的开销。
不过使用光栅化的时候需要注意以下几点:
- 如果 layer 不能被复用,则没有必要打开光栅化
- 如果 layer 不是静态,需要被频繁修改,比如处于动画之中,那么开启离屏渲染反而影响效率
- 离屏渲染缓存内容有时间限制,缓存内容 100ms 内如果没有被使用,那么就会被丢弃,无法进行复用
- 离屏渲染缓存空间有限,超过 2.5 倍屏幕像素大小的话也会失效,无法复用
离屏渲染的具体逻辑
图层的叠加绘制大概遵循“画家算法”,在这种算法下会按层绘制,首先绘制距离较远的场景,然后用绘制距离较近的场景覆盖较远的部分。
image-20200709105902023在普通的 layer 绘制中,上层的 sublayer 会覆盖下层的 sublayer,下层 sublayer 绘制完之后就可以抛弃了,从而节约空间提高效率。所有 sublayer 依次绘制完毕之后,整个绘制过程完成,就可以进行后续的呈现了。假设我们需要绘制一个三层的 sublayer,不设置裁剪和圆角,那么整个绘制过程就如下图所示:
image-20200709105945925而当我们设置了 cornerRadius 以及 masksToBounds 进行圆角 + 裁剪时,如前文所述,masksToBounds 裁剪属性会应用到所有的 sublayer 上。这也就意味着所有的 sublayer 必须要重新被应用一次圆角+裁剪,这也就意味着所有的 sublayer 在第一次被绘制完之后,并不能立刻被丢弃,而必须要被保存在 Offscreen buffer 中等待下一轮圆角+裁剪,这也就诱发了离屏渲染,具体过程如下:
image-20200709110035838实际上不只是圆角+裁剪,如果设置了透明度+组透明(layer.allowsGroupOpacity
+layer.opacity
),阴影属性(shadowOffset
等)都会产生类似的效果,因为组透明度、阴影都是和裁剪类似的,会作用与 layer 以及其所有 sublayer 上,这就导致会引起离屏渲染。
cornerRadius&masksToBounds一定会触发离屏渲染吗?
那么我是否思考过cornerRadius&masksToBounds就一定会触发离屏渲染吗?
接下来我们通过几个案例来验证下。
圆角+背景颜色
UIImageView *imageView = [[UIImageView alloc]init];
imageView.bounds = CGRectMake(0, 0, 100, 100);
imageView.center = self.view.center;
imageView.backgroundColor = [UIColor blueColor];
imageView.layer.cornerRadius = 50;
imageView.layer.masksToBounds = YES;
imageView.image = [UIImage imageNamed:@"timg"];
[self.view addSubview:imageView];
image-20200709111357461
结论:触发了离屏渲染
圆角+不设置背景色
UIImageView *imageView = [[UIImageView alloc]init];
imageView.bounds = CGRectMake(0, 0, 100, 100);
imageView.center = self.view.center;
imageView.layer.cornerRadius = 50;
imageView.layer.masksToBounds = YES;
imageView.image = [UIImage imageNamed:@"timg"];
[self.view addSubview:imageView];
image-20200709111545905
结论:未触发离屏渲染
按钮圆角+背景图片
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.bounds = CGRectMake(0, 0, 100, 100);
button.center = self.view.center;
button.layer.cornerRadius = 50;
[button setImage:[UIImage imageNamed:@"timg"] forState:UIControlStateNormal];
button.clipsToBounds = YES;
[self.view addSubview:button];
image-20200709111916178
结论:触发离屏渲染
按钮圆角+无背景图片
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.bounds = CGRectMake(0, 0, 100, 100);
button.center = self.view.center;
button.backgroundColor = [UIColor blueColor];
button.layer.cornerRadius = 50;
button.clipsToBounds = YES;
[self.view addSubview:button];
image-20200709112201789
结论:未触发离屏渲染
总结
上面的情景中,为什么有的没有触发离屏渲染,有的却触发了离屏渲染呢?
根据油画算法来看
-
当我们没有对图片设置背景颜色,只是单纯的设置了圆角和maskToBounds这个时候只需要先渲染内容,然后在内容的基础上裁减掉多余的内容即可,整个渲染过程,不需要额外的内存来保留之前的渲染效果,所以没有触发丽萍渲染。
-
当我们设置了背景颜色同时有设置了圆角,和图片。首先会将背景颜色渲染出来,并将其保存的内存中,再将图片内容渲染出来并保存在内存当中,最后再在背景色和图片内容上,将圆角渲染出来,在进行合成,最终显然到屏幕上。在这个过程中,我们需要额外的内存空间来进行临时内容的存储。所以触发了离屏渲染。
-
总之就是,当有多个图层同时涉及到圆角裁剪的时候,那么就需要额外的内存空间来保存这些图层的状态,这个时候就会触发离屏渲染。
避免圆角离屏渲染
由于刚才我们提到,圆角引起离屏渲染的本质是裁剪的叠加,导致 masksToBounds 对 layer 以及所有 sublayer 进行二次处理。那么我们只要避免使用 masksToBounds 进行二次处理,而是对所有的 sublayer 进行预处理,就可以只进行“画家算法”,用一次叠加就完成绘制。
那么可行的实现方法大概有下面几种:
- 【换资源】直接使用带圆角的图片,或者替换背景色为带圆角的纯色背景图,从而避免使用圆角裁剪。不过这种方法需要依赖具体情况,并不通用。
- 【mask】再增加一个和背景色相同的遮罩 mask 覆盖在最上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片或渐变色的情况。
- 【UIBezierPath】用贝塞尔曲线绘制闭合带圆角的矩形,在上下文中设置只有内部可见,再将不带圆角的 layer 渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是 layer 的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,所以需要对 frame、color 等进行手动地监听并重绘。
- 【CoreGraphics】重写
drawRect:
,用 CoreGraphics 相关方法,在需要应用圆角时进行手动绘制。不过 CoreGraphics 效率也很有限,如果需要多次调用也会有效率问题。 - 合理的使用cornerRadius+masksToBounds。