03-iOS底层原理|iOS OffScreen Rendering 离屏渲染原理

2,969 阅读10分钟

[TOC]

前言

一、离屏渲染具体过程

首先我们得了解一下渲染过程 和离屏渲染过程,为后面的认识做铺垫:

  • 通常的渲染
    • 根据前文,简化来看,通常的渲染流程是这样的:
    • App 通过 CPU 和 GPU 的合作,不停地将内容渲染完成放入 Framebuffer 帧缓冲器中,而显示屏幕不断地从 Framebuffer 中获取内容,显示实时的内容
  • 离屏渲染
    • 而离屏渲染的流程是这样的:
    • 与普通情况下 GPU 直接将渲染好的内容放入 Framebuffer 中不同,需要先额外创建离屏渲染缓冲区 Offscreen Buffer,将提前渲染好的内容放入其中,等到合适的时机再将 Offscreen Buffer 中的内容进一步叠加、渲染,完成后将结果切换到 Framebuffer 中

二、离屏渲染的效率问题

  • 从上面的流程来看,离屏渲染时由于 App 需要提前对部分内容进行额外的渲染并保存到 Offscreen Buffer,以及需要在必要时刻对 Offscreen Buffer 和 Framebuffer 进行内容切换,所以会需要更长的处理时间(实际上这两步关于 buffer 的切换代价都非常大)
  • 并且 Offscreen Buffer 本身就需要额外的空间,大量的离屏渲染可能早能内存的过大压力
  • 与此同时,Offscreen Buffer 的总大小也有限,不能超过屏幕总像素的 2.5 倍
  • 可见离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,很容易造成掉帧的问题
  • 所以大部分情况下,我们都应该尽量避免离屏渲染

三、为什么使用离屏渲染

那么为什么要使用离屏渲染呢?主要是因为下面这两种原因:

  1. 一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。
  2. 处于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。

被动触发

  • 对于第一种情况,也就是不得不使用离屏渲染的情况,一般都是系统自动触发的,比如阴影、圆角等等
    • 最常见的情形之一就是:使用了 mask 蒙版
    • 如图所示,由于最终的内容是由两层渲染结果叠加,所以必须要利用额外的内存空间对中间的渲染结果进行保存,因此系统会默认触发离屏渲染。
    • 又比如下面这个例子,iOS 8 开始提供的模糊特效 UIBlurEffectView:
    • 整个模糊过程分为多步:
      • Pass 1 先渲染需要模糊的内容本身
      • Pass 2 对内容进行缩放
      • Pass 3 4 分别对上一步内容进行横纵方向的模糊操作,最后一步用模糊后的结果叠加合成,最终实现完整的模糊特效

主动使用

  • 而第二种情况,为了复用提高效率而使用离屏渲染一般是主动的行为,是通过 CALayer 的 shouldRasterize 光栅化操作实现的。

四、shouldRasterize 光栅化

  • 开启光栅化后,会触发离屏渲染,Render Server 会强制将 CALayer 的渲染位图结果 bitmap 保存下来,这样下次再需要渲染时就可以直接复用,从而提高效率
  • 保存的 bitmap 包含 layer 的 subLayer、圆角、阴影、组透明度 group opacity 等,所以如果 layer 的构成包含上述几种元素,结构复杂且需要反复利用,那么就可以考虑打开光栅化
  • 圆角、阴影、组透明度等会由系统自动触发离屏渲染,那么打开光栅化可以节约第二次及以后的渲染时间。而多层 subLayer 的情况由于不会自动触发离屏渲染,所以相比之下会多花费第一次离屏渲染的时间,但是可以节约后续的重复渲染的开销
  • 不过使用光栅化的时候需要注意以下几点:
      1. 如果 layer 不能被复用,则没有必要打开光栅化
      1. 如果 layer 不是静态,需要被频繁修改,比如处于动画之中,那么开启离屏渲染反而影响效率
      1. 离屏渲染缓存内容有时间限制,缓存内容 100ms 内如果没有被使用,那么就会被丢弃,无法进行复用
      1. 离屏渲染缓存空间有限,超过 2.5 倍屏幕像素大小的话也会失效,无法复用

五、圆角的离屏渲染

  • 通常来讲,设置了 layer 的圆角效果之后,会自动触发离屏渲染。但是究竟什么情况下设置圆角才会触发离屏渲染呢?
  • 如上图所示,layer 由三层组成,我们设置圆角通常会首先像下面这行代码一样进行设置:
    view.layer.cornerRadius = 2
    
  • 根据 cornerRadius - Apple 的描述,上述代码只会默认设置 backgroundColor 和 border 的圆角,而不会设置 content 的圆角,除非同时设置了 layer.masksToBounds 为 true(对应 UIView 的 clipsToBounds 属性)
  • 如果只是设置了 cornerRadius 而没有设置 masksToBounds,由于不需要叠加裁剪,此时是并不会触发离屏渲染的。而当设置了裁剪属性的时候由于 masksToBounds 会对 layer 以及所有 subLayer 的 content 都进行裁剪,所以不得不触发离屏渲染 view.layer.masksToBounds = true // 触发离屏渲染的原因
  • 所以,Texture 也提出在没有必要使用圆角裁剪的时候,尽量不去触发离屏渲染而影响效率:

六、离屏渲染的具体逻辑

  • 刚才说了圆角加上 masksToBounds 的时候,因为 masksToBounds 会对 layer 上的所有内容进行裁剪,从而诱发了离屏渲染,那么这个过程具体是怎么回事呢,下面我们来仔细讲一下
  • 图层的叠加绘制大概遵循“画家算法”,在这种算法下会按层绘制,首先绘制距离较远的场景,然后用绘制距离较近的场景覆盖较远的部分
  • 在普通的 layer 绘制中,上层的 sublayer 会覆盖下层的 sublayer,下层 sublayer 绘制完之后就可以抛弃了,从而节约空间提高效率。
  • 所有 sublayer 依次绘制完毕之后,整个绘制过程完成,就可以进行后续的呈现了。假设我们需要绘制一个三层的 sublayer,不设置裁剪和圆角,那么整个绘制过程就如下图所示:
  • 而当我们设置了 cornerRadius 以及 masksToBounds 进行圆角 + 裁剪时,如前文所述,masksToBounds 裁剪属性会应用到所有的 sublayer 上。这也就意味着所有的 sublayer 必须要重新被应用一次圆角+裁剪,这也就意味着所有的 sublayer 在第一次被绘制完之后,并不能立刻被丢弃,而必须要被保存在 Offscreen buffer 中等待下一轮圆角+裁剪,这也就诱发了离屏渲染,具体过程如下:
  • 实际上不只是圆角+裁剪,如果设置了透明度+组透明(layer.allowsGroupOpacity+layer.opacity),阴影属性(shadowOffset 等)都会产生类似的效果,因为组透明度、阴影都是和裁剪类似的,会作用与 layer 以及其所有 sublayer 上,这就导致必然会引起离屏渲染

七、避免圆角离屏渲染

  • 除了尽量减少圆角裁剪的使用,还有什么别的办法可以避免圆角+裁剪引起的离屏渲染吗?
  • 由于刚才我们提到,圆角引起离屏渲染的本质裁剪的叠加,导致 masksToBounds 对 layer 以及所有 sublayer 进行二次处理。那么我们只要避免使用 masksToBounds 进行二次处理,而是对所有的 sublayer 进行预处理,就可以只进行“画家算法”,用一次叠加就完成绘制
  • 那么可行的实现方法大概有下面几种:
    • 1.【换资源】直接使用带圆角的图片,或者替换背景色为带圆角的纯色背景图,从而避免使用圆角裁剪。不过这种方法需要依赖具体情况,并不通用
    • 2.【mask】再增加一个和背景色相同的遮罩 mask 覆盖在最上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片或渐变色的情况
    • 3.【UIBezierPath】用贝塞尔曲线绘制闭合带圆角的矩形,在上下文中设置只有内部可见,再将不带圆角的 layer 渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是 layer 的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,所以需要对 frame、color 等进行手动地监听并重绘。
    • 4.【CoreGraphics】重写 drawRect:,用 CoreGraphics 相关方法,在需要应用圆角时进行手动绘制。不过 CoreGraphics 效率也很有限,如果需要多次调用也会有效率问题

八、触发离屏渲染原因的总结

  • 总结一下,下面几种情况会触发离屏渲染:
    • 使用了 mask 的 layer (layer.mask)
    • 需要进行裁剪的 layer (layer.masksToBounds / view.clipsToBounds)
    • 设置了组透明度为 YES,并且透明度不为 1 的 layer (layer.allowsGroupOpacity/layer.opacity)
    • 添加了投影的 layer (layer.shadow)
    • 采用了光栅化的 layer (layer.shouldRasterize)
    • 绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)
  • 不过,需要注意的是,重写 drawRect: 方法并不会触发离屏渲染。前文中我们提到过,重写 drawRect: 会将 GPU 中的渲染操作转移到 CPU 中完成,并且需要额外开辟内存空间。但根据苹果工程师的说法,这和标准意义上的离屏渲染并不一样,在 Instrument 中开启 Color offscreen rendered yellow 调试时也会发现这并不会被判断为离屏渲染。

相关阅读(共计14篇文章)

iOS相关专题

webApp相关专题

跨平台开发方案相关专题

阶段性总结:Native、WebApp、跨平台开发三种方案性能比较

Android、HarmonyOS页面渲染专题

小程序页面渲染专题

[TOC]