iOS 真实的离屏渲染

2020-07-09  本文已影响0人  Joker_King

我们都知道在iOS中离屏渲染会影响性能,它是在GPU的帧缓冲区(framebuff)之外额外的开辟了一段内存空间来进行渲染的,那么它为什么需要这样做呢?

离屏渲染具体过程

简单理解,通常的渲染流程可简化为下面这个样子。

image-20200709095311191

App 通过 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 倍。

可见离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,很容易造成掉帧的问题。所以大部分情况下,我们都应该尽量避免离屏渲染。

为什么要使用离屏渲染

既然离屏渲染开销巨大,那么我们为什么还要使用离屏渲染呢?

自动触发

mask

对于第一种情况,也就是不得不使用离屏渲染的情况,一般都是系统自动触发的,比如阴影、圆角等等。

最常见的情形之一就是:使用了 mask 蒙版。

image-20200709095841419

由于最终的结果是由两个图层叠加而成,所以需要额外的内存空间来存储这两个图层的中间状态。在合适的时机来合成为最终的图层。因此系统会自动触发离屏渲染。

UIBlurEffectView

再比如iOS 8 开始提供的模糊特效 UIBlurEffectView:

image-20200709102104888 image-20200709102141651

模糊效果主要分为以下几个步骤:

而这个过程,需要保存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 的情况由于不会自动触发离屏渲染,所以相比之下会多花费第一次离屏渲染的时间,但是可以节约后续的重复渲染的开销。

不过使用光栅化的时候需要注意以下几点:

  1. 如果 layer 不能被复用,则没有必要打开光栅化
  2. 如果 layer 不是静态,需要被频繁修改,比如处于动画之中,那么开启离屏渲染反而影响效率
  3. 离屏渲染缓存内容有时间限制,缓存内容 100ms 内如果没有被使用,那么就会被丢弃,无法进行复用
  4. 离屏渲染缓存空间有限,超过 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

结论:未触发离屏渲染

总结

上面的情景中,为什么有的没有触发离屏渲染,有的却触发了离屏渲染呢?

根据油画算法来看

避免圆角离屏渲染

由于刚才我们提到,圆角引起离屏渲染的本质是裁剪的叠加,导致 masksToBounds 对 layer 以及所有 sublayer 进行二次处理。那么我们只要避免使用 masksToBounds 进行二次处理,而是对所有的 sublayer 进行预处理,就可以只进行“画家算法”,用一次叠加就完成绘制。

那么可行的实现方法大概有下面几种:

  1. 【换资源】直接使用带圆角的图片,或者替换背景色为带圆角的纯色背景图,从而避免使用圆角裁剪。不过这种方法需要依赖具体情况,并不通用。
  2. 【mask】再增加一个和背景色相同的遮罩 mask 覆盖在最上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片或渐变色的情况。
  3. 【UIBezierPath】用贝塞尔曲线绘制闭合带圆角的矩形,在上下文中设置只有内部可见,再将不带圆角的 layer 渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是 layer 的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,所以需要对 frame、color 等进行手动地监听并重绘。
  4. 【CoreGraphics】重写 drawRect:,用 CoreGraphics 相关方法,在需要应用圆角时进行手动绘制。不过 CoreGraphics 效率也很有限,如果需要多次调用也会有效率问题。
  5. 合理的使用cornerRadius+masksToBounds。
上一篇下一篇

猜你喜欢

热点阅读