003---离屛渲染
Offscreen Rendering 离屏渲染
离屏渲染具体过程
通常的渲染流程是这样的:
image.png
App通过CPU和GPU的合作,不停地将内容渲染完成放入Framebuffer帧缓冲器中,而显示屏幕不断地从Framebuffer中获取内容,显示实时的内容
而离屏渲染的流程是这样的:
image.png
与普通情况下
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 蒙版。
image.png
如图所示,由于最终的内容是由
两层渲染结果叠加,所以必须要利用额外的内存空间对中间的渲染结果进行保存,因此系统会默认触发离屏渲染。
-
又比如下面这个例子,iOS 8 开始提供的模糊特效
UIBlurEffectView: -
水平和垂直修改一下,上图中错误
image.png
image.png
整个模糊过程分为多步:
Pass 1先渲染需要模糊的内容本身,Pass 2对内容进行缩放,Pass 3 4分别对上一步内容进行横纵方向的模糊操作,最后一步用模糊后的结果叠加合成,最终实现完整的模糊特效。
而第二种情况,为了复用提高效率而使用离屏渲染一般是主动的行为,是通过
CALayer的shouldRasterize 光栅化操作实现的。
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倍屏幕像素大小的话也会失效,无法复用
圆角的离屏渲染
通常来讲,设置了
layer的圆角效果之后,会自动触发离屏渲染。但是究竟什么情况下设置圆角才会触发离屏渲染呢?
image.png
如上图所示,
layer由三层组成,我们设置圆角通常会首先像下面这行代码一样进行设置:
view.layer.cornerRadius = 2
根据 cornerRadius - Apple 的描述,上述代码只会默认设置
backgroundColor和border的圆角,而不会设置content的圆角,除非同时设置了layer.masksToBounds为true(对应UIView的clipsToBounds属性):
Setting the radius to a value greater than
0.0causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contentsproperty; it applies only to the background color and border of the layer. However, setting themasksToBoundsproperty totruecauses the content to be clipped to the rounded corners.
如果只是设置了
cornerRadius而没有设置masksToBounds,由于不需要叠加裁剪,此时是并不会触发离屏渲染的。而当设置了裁剪属性的时候,由于masksToBounds会对layer以及所有subLayer的content都进行裁剪,所以不得不触发离屏渲染。
view.layer.masksToBounds = true // 触发离屏渲染的原因
所以,
Texture也提出在没有必要使用圆角裁剪的时候,尽量不去触发离屏渲染而影响效率:
image.png
离屏渲染的具体逻辑
刚才说了圆角加上
masksToBounds的时候,因为masksToBounds会对layer上的所有内容进行裁剪,从而诱发了离屏渲染,那么这个过程具体是怎么回事呢,下面我们来仔细讲一下。
- 图层的叠加绘制大概遵循“画家算法”,在这种算法下会按层绘制,首先绘制距离
较远的场景,然后用绘制距离较近的场景覆盖较远的部分.
image.png
- 在普通的
layer绘制中,上层的 sublayer会覆盖下层的 sublayer,下层 sublayer绘制完之后就可以抛弃了,从而节约空间提高效率。所有sublayer依次绘制完毕之后,整个绘制过程完成,就可以进行后续的呈现了。假设我们需要绘制一个三层的 sublayer,不设置裁剪和圆角,那么整个绘制过程就如下图所示:
image.png
而当我们设置了
cornerRadius以及masksToBounds进行圆角 + 裁剪时,如前文所述,masksToBounds裁剪属性会应用到所有的 sublayer 上。这也就意味着所有的 sublayer 必须要重新被应用一次圆角+裁剪,这也就意味着所有的 sublayer 在第一次被绘制完之后,并不能立刻被丢弃,而必须要被保存在 Offscreen buffer 中等待下一轮圆角+裁剪,这也就诱发了离屏渲染,具体过程如下:
image.png
实际上不只是
圆角+裁剪,如果设置了透明度+组透明(layer.allowsGroupOpacity+layer.opacity),阴影属性(shadowOffset等)都会产生类似的效果,因为组透明度、阴影都是和裁剪类似的,会作用与layer以及其所有sublayer上,这就导致必然会引起离屏渲染。
避免圆角离屏渲染
除了尽量减少圆角裁剪的使用,还有什么别的办法可以避免圆角+裁剪引起的离屏渲染吗
由于刚才我们提到,圆角引起离屏渲染的本质是裁剪的叠加,导致
masksToBounds对layer以及所有sublayer进行二次处理。那么我们只要避免使用masksToBounds进行二次处理,而是对所有的sublayer进行预处理,就可以只进行“画家算法”,用一次叠加就完成绘制。
那么可行的实现方法大概有下面几种:
- 【换资源】直接使用带圆角的图片,或者替换背景色为带圆角的纯色背景图,从而避免使用圆角裁剪。不过这种方法需要依赖具体情况,并不通用。
- 【
mask】再增加一个和背景色相同的遮罩mask覆盖在最上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片或渐变色的情况。 - 【
UIBezierPath】用贝塞尔曲线绘制闭合带圆角的矩形,在上下文中设置只有内部可见,再将不带圆角的layer渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是layer的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,所以需要对frame、color等进行手动地监听并重绘。 - 【
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 调试时也会发现这并不会被判断为离屏渲染。
UIButton设置圆角离屛渲染问题
btn1.layer.cornerRadius = 50;
btn1.backgroundColor = [UIColor blueColor];
btn1.clipsToBounds = YES;
image.png
给Button设置圆角,如果Button的imageView设置圆角会发生离屛渲染.
btn1.imageView.layer.cornerRadius = 50;
image.png
如果只给Button的imageView设置圆角不会触发离屛渲染.
image.png