iOS离屏渲染
离屏渲染的定义
- 如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的
frame buffer
,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer
,而是先暂存在另外的内存区域,之后再写入frame buffer
,那么这个过程被称之为离屏渲染。
离屏渲染的由来
图像/图形渲染的流程:
GPU进⾏渲染-> 帧缓存区⾥ -> 视频控制器 -> 读取帧缓存区信息(位图) -> 数模转化(数字信号处->模 拟型号) -> (逐⾏扫描)显示. 重点来了:当帧缓冲区的数据不能直接被视频控制器扫描显示的时候,我们要额外的开辟一个缓冲区 -------> 离屏缓冲区来存储我们不能第一时间交给视频控制器显示的数据,在离屏缓冲区渲染好我们不能直接被视频控制器显示的数据,等到最终我们可以确认当前的VIew到底怎么显示之后,再交给帧缓冲区 -----> 视频控制器显示。
离屏渲染的代价很高,想要进行离屏渲染,首选要创建一个新的缓冲区,屏幕渲染会有一个上下文环境的一个概念,离屏渲染的整个过程需要切换上下文环境,先从当前屏幕切换到离屏,等结束后,又要将上下文环境切换回来。这也是为什么会消耗性能的原因了
(如果出现了这两行代码,会给tableVIew的渲染以及滑动带来什么影响?)
btn.layer.cornerRadius = 50;
btn.clipsToBounds = YES;
离屏渲染有哪些问题
- 内存开支:开辟离屏缓冲区(大小不超过2.5倍屏幕像素大小)
- 时间和性能开支:从离屏缓冲区拷贝数据到帧缓冲区,上下文切换耗性能
如何检测项⽬目中那些图层触发了了离屏渲染问题
我们写下如下代码:
上述代码,可以看到1和3产生了离屏渲染
[图片上传失败...(image-d55898-1594131146671)]
首先普及一下CALayer的层次结构:CALayer由背景色backgroundColor、内容contents、边缘borderWidth&borderColor构成
cornerRadius的文档中明确说明对cornerRadius的设置只对 CALayer 的backgroundColor和borderWidth&borderColor起作用,如果contents有内容或者内容的背景不是透明的话,只有设置masksToBounds为 true 才能起作用,此时两个属性相结合,产生离屏渲染。
这也就说明了上面代码为什么1和3触发了离屏渲染,而2和4没有触发离屏渲染
解决办法:
后台绘制圆角图片,前台进行设置:
(2)对于 contents 无内容或者内容的背景透明(无涉及到圆角以外的区域)的layer,直接设置layer的 backgroundColor 和 cornerRadius 属性来绘制圆角。
(3)使用混合图层,在layer上方叠加相应mask形状的半透明layer
sublayer.contents=(id)[UIImage imageNamed:@"xxx"].CGImage;
[view.layer addSublayer:sublayer];
其他情况触发离屏渲染以及解决办法:
- mask(遮罩)------> 使用混合图层,在layer上方叠加相应mask形状的半透明layer
2.edge antialiasing(抗锯齿)----->不设置 allowsEdgeAntialiasing 属性为YES(默认为NO)
- allowsGroupOpacity(组不透明,开启CALayer的allowsGroupOpacity属性后,子 layer 在视觉上的透明度的上限是其父 layer 的opacity(对应UIView的alpha),并且从 iOS 7 以后默认全局开启了这个功能,这样做是为了让子视图与其容器视图保持同样的透明度。)------->关闭 allowsGroupOpacity 属性,按产品需求自己控制layer透明度
4.shadows(阴影)------> 设置阴影后,设置CALayer的 shadowPath,view.layer.shadowPath=[UIBezierPath pathWithCGRect:view.bounds].CGPath;
CALayer离屏渲染终极解决方案:当视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES(缓存离屏渲染的数据,当下次用到的时候直接拿,不需要开辟新的离屏缓冲区),此方案最为实用方便。
view.layer.shouldRasterize = true;view.layer.rasterizationScale = view.layer.contentsScale;
shouldRasterize (光栅华使用建议):
1.如果layer不需要服用,则没有必要打开
2.如果layer不是静态的,需要被频繁修改,比如出于动画之中,则开启光栅华反而影响性能
3.离屏渲染缓存有时间限制,当超过100ms,内容没有被使用就会被丢弃,无法复用
4.离屏渲染缓存有空间限制,超过屏幕像素的2.5倍则失效,并无法使用
善用离屏渲染
尽管离屏渲染开销很大,但是当我们无法避免它的时候,可以想办法把性能影响降到最低。优化思路也很简单:既然已经花了不少精力把图片裁出了圆角,如果我能把结果缓存下来,那么下一帧渲染就可以复用这个成果,不需要再重新画一遍了。
CALayer为这个方案提供了对应的解法:shouldRasterize。一旦被设置为true,Render Server就会强制把layer的渲染结果(包括其子layer,以及圆角、阴影、group opacity等等)保存在一块内存中,这样一来在下一帧仍然可以被复用,而不会再次触发离屏渲染。有几个需要注意的点:
1.shouldRasterize的主旨在于降低性能损失,但总是至少会触发一次离屏渲染。如果你的layer本来并不复杂,也没有圆角阴影等等,打开这个开关反而会增加一次不必要的离屏渲染
2.离屏渲染缓存有空间上限,最多不超过屏幕总像素的2.5倍大小
3.一旦缓存超过100ms没有被使用,会自动被丢弃
4.layer的内容(包括子layer)必须是静态的,因为一旦发生变化(如resize,动画),之前辛苦处理得到的缓存就失效了。如果这件事频繁发生,我们就又回到了“每一帧都需要离屏渲染”的情景,而这正是开发者需要极力避免的。针对这种情况,Xcode提供了“Color Hits Green and Misses Red”的选项,帮助我们查看缓存的使用是否符合预期
5.其实除了解决多次离屏渲染的开销,shouldRasterize在另一个场景中也可以使用:如果layer的子结构非常复杂,渲染一次所需时间较长,同样可以打开这个开关,把layer绘制到一块缓存,然后在接下来复用这个结果,这样就不需要每次都重新绘制整个layer树了
总结:
(1)离屏渲染是系统触发,触发了之后才有离屏缓冲区,离屏缓冲区和我上一篇文章讲到的帧缓冲区的二级缓冲机制没有任何的因果关系
(2)btn.layer.cornerRadius = 50&&btn.clipsToBounds = YES不一定会触发离屏渲染,cornerRadius的文档中明确说明对cornerRadius的设置只对 CALayer 的backgroundColor和borderWidth&borderColor起作用,如果contents有内容或者内容的背景不是透明的话,只有设置masksToBounds为 true 才能起作用,此时两个属性相结合,产生离屏渲染
(3)在uitableVIewcell触发了离屏渲染,会导致在滑动的时候高频率的开辟离屏缓冲区,这样就会造成tanleView滑动卡顿,如果视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES,此方案最为实用方便,但是当视图内容是动态变化(如后台下载图片完毕后切换到主线程设置)时,使用此方案反而为增加系统负荷。
(4)现在摆在我们面前得有三个选择:当前屏幕渲染、离屏渲染、CPU渲染,该用哪个呢?这需要根据具体的使用场景来决定。· 尽量使用当前屏幕渲染,鉴于离屏渲染、CPU渲染可能带来的性能问题,一般情况下,我们要尽量使用当前屏幕渲染。离屏渲染 VS CPU渲染