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 蒙版。
如图所示,由于最终的内容是由
两层渲染
结果叠加,所以必须要利用额外的内存空间对中间的渲染结果进行保存,因此系统会默认触发离屏渲染。
-
又比如下面这个例子,iOS 8 开始提供的模糊特效
UIBlurEffectView
: -
水平和垂直修改一下,上图中错误
整个模糊过程分为多步:
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
倍屏幕像素大小的话也会失效,无法复用
圆角的离屏渲染
image.png通常来讲,设置了
layer
的圆角效果之后,会自动触发离屏渲染。但是究竟什么情况下设置圆角才会触发离屏渲染呢?
如上图所示,
layer
由三层组成,我们设置圆角通常会首先像下面这行代码一样进行设置:
view.layer.cornerRadius = 2
根据 cornerRadius - Apple 的描述,上述代码只会默认设置
backgroundColor
和border
的圆角,而不会设置content
的圆角,除非同时设置了layer.masksToBounds
为true
(对应UIView
的clipsToBounds
属性):
Setting the radius to a value greater than
0.0
causes 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 contents
property; it applies only to the background color and border of the layer. However, setting themasksToBounds
property totrue
causes the content to be clipped to the rounded corners.
如果只是设置了
cornerRadius
而没有设置masksToBounds
,由于不需要叠加裁剪,此时是并不会触发离屏渲染的。而当设置了裁剪属性的时候,由于masksToBounds
会对layer
以及所有subLayer
的content
都进行裁剪,所以不得不触发离屏渲染。
view.layer.masksToBounds = true // 触发离屏渲染的原因
image.png所以,
Texture
也提出在没有必要使用圆角裁剪的时候,尽量不去触发离屏渲染而影响效率:
离屏渲染的具体逻辑
刚才说了圆角加上
masksToBounds
的时候,因为masksToBounds
会对layer
上的所有内容进行裁剪,从而诱发了离屏渲染,那么这个过程具体是怎么回事呢,下面我们来仔细讲一下。
- 图层的叠加绘制大概遵循“画家算法”,在这种算法下会按层绘制,首先绘制距离
较远
的场景,然后用绘制距离较近的场景覆盖较远
的部分.
- 在普通的
layer
绘制中,上层的 sublayer
会覆盖下层的 sublayer
,下层 sublayer
绘制完之后就可以抛弃了,从而节约空间提高效率。所有sublayer
依次绘制完毕之后,整个绘制过程完成,就可以进行后续的呈现了。假设我们需要绘制一个三层的 sublayer,不设置裁剪和圆角,那么整个绘制过程就如下图所示:
而当我们设置了
image.pngcornerRadius
以及masksToBounds
进行圆角 + 裁剪时,如前文所述,masksToBounds
裁剪属性会应用到所有的 sublayer 上。这也就意味着所有的 sublayer 必须要重新被应用一次圆角+裁剪,这也就意味着所有的 sublayer 在第一次被绘制完之后,并不能立刻被丢弃,而必须要被保存在 Offscreen buffer 中等待下一轮圆角+裁剪,这也就诱发了离屏渲染,具体过程如下:
实际上不只是
圆角+裁剪
,如果设置了透明度+组透明
(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设置圆角不会触发离屛渲染.