上一篇 我们介绍了图像从数据到屏幕的渲染过程,现在我们来研究一下iOS的离屏渲染。那我们先来看一下渲染模式,iOS下有两种渲染模式:在屏渲染和离屏渲染。
一、在屏渲染(On-Screen Rendering)/离屏渲染(Off-Screen Rendering)
在屏渲染: 上一篇讲到:GPU渲染的数据会放在帧缓冲区,然后视频控制器从帧缓冲区读取数据显示到屏幕上。GPU渲染使用的内存是帧缓冲区(GPU和显存共享物理内存),就是在屏渲染。
离屏渲染: GPU在当前帧缓冲区以外开辟一个缓冲区进行渲染操作。意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。GPU另外开辟的空间是有限制的,最大为屏幕像素点的2.5倍。
按照这样的说法,如果将不在GPU的当前屏幕缓冲区中进行的渲染都称为离屏渲染,那么就还有另一种特殊的“离屏渲染”方式:CPU渲染。如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内同步地完成,渲染得到的bitmap最后再交由GPU用于显示。但是这个过程并不会被Xcode识别。如果你的view实现了drawRect,此时打开Xcode调试的“Color offscreen rendered yellow”开关,你会发现这片区域不会被标记为黄色,说明Xcode并不认为这属于离屏渲染。
二、为什么要离屏渲染
iOS主要的渲染操作都是由CoreAnimation的Render Server模块,通过调用显卡驱动所提供的OpenGL/Metal接口来执行的。通常对于每一层layer,Render Server会遵循“画家算法”,按次序输出到frame buffer,后一层覆盖前一层,就能得到最终的显示结果。
但是某些场景并没有那么简单。GPU虽然可以一层一层往画布上进行输出,但是无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分——因为在这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖了。比如需要在一张图片上加个遮罩效果,这样就需要有个状态记录上一次图片的内容,将遮罩与图片进行混合,然后再显示出来。这个动作就需要对于每一层layer,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂的、多次的修改/剪裁操作。(由系统自动触发)
另外,离屏渲染在某些情况带来效率提升。比如一个熏染效果需要重复使用,那么我们就提前渲染好放在OffscreenBuffer,达到复用目的。(开发者手动触发)
三、什么情况会触发GPU离屏渲染?
1、 圆角效果:必须满足条件:cornerRadius+clipsToBounds+存在显示的子图层
设置cornerRadius+clipsToBounds就一定会触发离屏渲染么?如果你草率回答是,那么面试估计就GG了。
iOS 9.0 之前:UIimageView跟UIButton设置圆角都会触发离屏渲染。
iOS 9.0 之后:UIImageView里仅图片设置圆角不会触发离屏渲染了。如果设置其他背景、阴影效果之类的还是会触发离屏渲染的。UIbutton仅背景颜色和边框情况下设置圆角不会触发离屏渲染。如果设置其他背景图片、阴影效果之类的还是会触发离屏渲染的。
2、阴影
其原因在于,虽然layer本身是一块矩形区域,但是阴影默认是作用在其中”非透明区域“的,而且需要显示在所有layer内容的下方,因此根据画家算法必须被渲染在先。但矛盾在于此时阴影的本体(layer和其子layer)都还没有被组合到一起,怎么可能在第一步就画出只有完成最后一步之后才能知道的形状呢?这样一来又只能另外申请一块内存,把本体内容都先画好,再根据渲染结果的形状,添加阴影到frame buffer,最后把内容画上去。不过如果我们能够预先告诉CoreAnimation(通过shadowPath属性)阴影的几何形状,那么阴影当然可以先被独立渲染出来,不需要依赖layer本体,也就不再需要离屏渲染了。
3、group opacity(透明度)
其实从名字就可以猜到,alpha并不是分别应用在每一层之上,而是只有到整个layer树画完之后,再统一加上alpha,最后和底下其他layer的像素进行组合。显然也无法通过一次遍历就得到最终结果。将一对蓝色和红色layer叠在一起,然后在父layer上设置opacity=0.5,并复制一份在旁边作对比。左边关闭group opacity,右边保持默认(从iOS7开始,如果没有显式指定,group opacity会默认打开),然后打开offscreen rendering的调试,我们会发现右边的那一组确实是离屏渲染了。
4、mask(遮罩)
我们知道mask是应用在layer和其所有子layer的组合之上的,而且可能带有透明度,那么其实和group opacity的原理类似,不得不在离屏渲染中完成。
5、UIBlurEffect(模糊效果)
渲染的位图并不能直接给帧缓存区等待显示,而要经过模糊处理之后才能将最后的渲染数据 -> 帧缓冲区-> 显示。同样无法通过一次遍历完成,其原理在WWDC中提到
6、shouldRasterize(光栅化)
为图层设置layer.shouldRasterize=true
7、 edge antialiasing(抗锯齿)
8、颜色渐变
四、CPU离屏渲染
使用CGContext在drawRect :方法中绘制大部分情况下会导致离屏渲染文本(任何种类,包括UILabel,CATextLayer,Core Text等)。
五、GPU离屏渲染的性能影响
(1)创建新缓冲区
要想进行离屏渲染,首先要创建一个新的缓冲区。
(2)上下文切换
离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
六、如何使用离屏渲染
1、 圆角优化:
- 如果Button单纯的需要圆角效果,不需要裁剪,则不设置button.clipsToBounds 就不会触发离屏渲染。
- 使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角。
- 使用CAShapeLayer和UIBezierPath设置圆角。
2、shadow优化
对于shadow,如果图层是个简单的几何图形或者圆角图形,我们可以通过设置shadowPath来优化性能,能大幅提高性能。
3、shouldRasterize (光栅化使用建议)
- 如果layer不能被复用,则没有必要打开光栅化。
- 如果layer不是静态的,需要别频繁的修改,比如处于动画之中,开启光栅化反而会影响效率。
- 离屏渲染缓存内容有时间限制,缓存内容100ms内如果没有被使用,就会被丢弃,无法被复用了。
- 离屏渲染缓存空间有限,超过2.5倍屏幕像素大小,也会失效,无法进行复用。
4、其他建议
- 当我们需要圆角效果时,可以使用一张中间透明图片蒙上去
- 使用ShadowPath指定layer阴影效果路径
- 使用异步进行layer渲染(Facebook开源的异步绘制框架AsyncDisplayKit)
- 设置layer的opaque值为YES,减少复杂图层合成
- 尽量使用不包含透明(alpha)通道的图片资源
- 尽量设置layer的大小值为整形值
- 直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
- 很多情况下用户上传图片进行显示,可以让服务端处理圆角
- 使用代码手动生成圆角Image设置到要显示的View上,利用UIBezierPath(CoreGraphics框架)画出来圆角图
- 渲染不是CPU的强项,调用CoreGraphics会消耗其相当一部分计算时间,并且我们也不愿意因此阻塞用户操作,因此一般来说CPU渲染都在后台线程完成,然后再回到主线程上,把渲染结果传回CoreAnimation。这样一来,多线程间数据同步会增加一定的复杂度。同样因为CPU渲染速度不够快,因此只适合渲染静态的元素,如文字。
- 作为渲染结果的bitmap数据量较大(形式上一般为解码后的UIImage),消耗内存较多,所以应该在使用完及时释放,并在需要的时候重新生成,否则很容易导致OOM。
- 如果你选择使用CPU来做渲染,那么就没有理由再触发GPU的离屏渲染了,否则会同时存在两块内容相同的内存,而且CPU和GPU都会比较辛苦
一定要使用Instruments的不同工具来测试性能,而不是仅凭猜测来做决定。
七、如何测试离屏渲染
1、Instruments 卡顿监测
Time Profiler ->Call Tree Options :
1. Separete By Thread :按线程划分
2. Invert Call Tree :逆向调用树,方便查看调用顺序
3. Hide System Libraries:隐藏系统库
Core Animation ->Debug Options :
1. Color Blended Layers :监测图层混合情况,没有混合的部分为绿色,混合最严重的部分是红色,大量图层混合会消耗GPU的时间。
2. Color Copied Images :监测图片颜色格式,如果GPU不支持当前图片的颜色格式,会将其交给CPU预先进行格式转化,并且这张图片被标记为蓝色。(Apple 的 GPU值解析32bit的颜色格式,RGBA)
3. Color Immediately :设置调试颜色每帧更新。(一般不用)
4. Color Compositing-Fast-Path Blue :对任何直接使用OpenGL绘制的图层高亮。
5. Flash Updated Regions :对重绘的内容高亮成黄色。(使用Core Graphics绘制的图层)
6. Color Hits Green and Misses Red :光栅化监测,前面已述。
7. Color Offscreen-Renderded Yellow :离屏渲染监测,前面已述。
8. Color Non-Standard Surface Formats:Apple 文档没注解(一般不用)
参考文章