[TOC]
前言
我们前期在探索iOS动画及渲染相关原理 的时候,先后了解了计算机图形渲染原理 、 移动终端屏幕成像与卡顿原理、iOS的各个渲染框架以及iOS图层渲染原理 ;
我们作为一个程序员,在提升技术能力水平的时候,有几种学习方法,比如,链式学习法、环式学习法、比较学习法等。很明显,我们在探索iOS动画及渲染相关原理 的时候用的就是链式学习法。紧接着,为了拓宽自己的知识面,对相同课题的做一个相对系统的认识,我们将采用环式学习法,去探索在iOS领域相关的几个课题:
- iOS-OffScreenRendering离屏渲染原理
- iOS因CPU、GPU资源消耗导致卡顿的原因和解决方案
那我们就直接进入我们今天的第一个新课题,OffScreenRendering离屏渲染原理吧
一、离屏渲染具体过程
首先我们得了解一下渲染过程 和离屏渲染过程,为后面的认识做铺垫:
- 通常的渲染
- 根据前文,简化来看,通常的渲染流程是这样的:
- 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 倍
- 可见
离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,很容易造成掉帧的问题
- 所以
大部分情况下,我们都应该尽量避免离屏渲染
三、为什么使用离屏渲染
那么为什么要使用离屏渲染呢?主要是因为下面这两种原因:
- 一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。
- 处于效率目的,可以将内容提前渲染保存在 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 的情况由于不会自动触发离屏渲染,所以相比之下会多花费第一次离屏渲染的时间,但是可以节约后续的重复渲染的开销
- 不过使用光栅化的时候需要注意以下几点:
-
- 如果
layer 不能被复用,则没有必要
打开光栅化
- 如果
-
如果 layer 不是静态,需要被频繁修改
,比如处于动画之中,那么开启离屏渲染反而影响效率
-
离屏渲染缓存内容有时间限制
,缓存内容 100ms 内如果没有被使用,那么就会被丢弃,无法进行复用
-
离屏渲染缓存空间有限,超过 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 效率也很有限,如果需要多次调用也会有效率问题
。
- 1.【换资源】
八、触发离屏渲染原因的总结
- 总结一下,下面几种情况会触发离屏渲染:
- 使用了 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相关专题
- 01-iOS底层原理|iOS的各个渲染框架以及iOS图层渲染原理
- 02-iOS底层原理|iOS动画渲染原理
- 03-iOS底层原理|iOS OffScreen Rendering 离屏渲染原理
- 04-iOS底层原理|因CPU、GPU资源消耗导致卡顿的原因和解决方案
webApp相关专题
跨平台开发方案相关专题
阶段性总结:Native、WebApp、跨平台开发三种方案性能比较
Android、HarmonyOS页面渲染专题
小程序页面渲染专题
[TOC]