【iOS重学】离屏渲染
屏幕显示完整流程
整体渲染流程可以分为三个阶段:
CPU的计算主要是通过CoreAnimation来处理,通过OpenGL ES/Metal将数据传递给GPU。
GPU渲染主要是将接收到的渲染数据进行一系列渲染之后将帧数据存储在帧缓存(Frame Buffer)里面,供视频控制器调用。
视频控制器从帧缓存中获取到帧数据显示在屏幕上。
屏幕显示图像原理
CRT显示器原理
CRT显示器原理主要是通过【电子束】激发屏幕内表面的荧光粉来显示图像,由于荧光粉点亮后很快就会熄灭,所以【电子枪】需要不断的【从上到下】进行扫描,扫描完成后显示器就呈现一帧画面,电子枪回到【初始位置】开始下一次的扫描。
GPU渲染完成后将渲染结果存入帧缓存区,视频控制器根据【垂直同步信号】逐帧读取帧缓冲区的数据,经过数据转换之后由显示器进行显示。
帧缓存(Frame Buffer)
Refresh Rate,单位hz,指的是设备刷新屏幕的频率,这个频率一般是60hz,所以每隔16.67ms屏幕会刷新一次。
Frame Rate,单位fps,指的是GPU生成帧的速率。
也叫显存,它是屏幕所显示画面的一个直接映像,也叫做位映射图(bitmap)或光栅,帧缓存的每一存储单元对应屏幕上一个像素,整个帧缓存对应一帧图像。
图像撕裂
当【帧率】大于【屏幕刷新频率】时,当视频控制器刚读完一帧的上半部分时,GPU已经把下一帧准备好并提交到帧缓存,这样视频控制器就会读到下一帧的下半部分在屏幕显示。
苹果使用的是【双缓存】和【垂直同步信号】。
卡顿
当显示器的【垂直同步信号】发出的时候,GPU没有完成相应的渲染就会出现【卡顿】的现象,这也是为了解决画面撕裂的问题带来的副作用,如下图所示:
离屏渲染
什么是离屏渲染?
当GPU无法直接把渲染结果存放到帧缓存中,而是先是暂时把中间的一个临时状态存放在另外的区域。之后再存放到帧缓存,这个过程叫离屏渲染。
即是说:GPU需要再当前屏幕缓存区以外开辟一个新的缓冲区进行操作。
当前屏幕渲染和离屏渲染
离屏渲染的性能损耗
离屏渲染在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作,造成其性能损耗的主要原因在于:创建离屏渲染和上下文切换。
切换上下文主要是当发生离屏渲染时,渲染上下文需从当前屏幕缓冲区切换到屏幕外缓冲区然后再完成渲染。
如果一屏元素都发生离屏渲染,这个从当前屏幕缓冲区切换到屏幕外的缓冲区就会发生多次,自然就会有一定的性能损耗。
常见的离屏渲染场景
设置圆角
满足以下条件的就会发生离屏渲染:
- clipsToBounds开启,圆角 > 0,contents上有内容
- 同时修改了contents+backgroundColor 或 contents+border(iOS9之后)
- 直接让UI提供带圆角的图片
- 利用UIBezierPath和CAShapeLayer
- 利用UIBezierPath和CoreGraphics
设置遮罩
- 渲染layer的mask纹理
- 渲染layer的content纹理
- 合并操作:合并mask 和 content纹理
满足以下条件的都会触发离屏渲染:
- 设置了mask + 任意contents(比如设置UILabel文字、背景颜色、图片等)
设置阴影(shadow)
阴影的本质和layer类似,都是在layer下一层多添加一层,根据前面提到的【画家算法】无法一次性生成,所以会发生离屏渲染。
1.利用UIBezierPath给视图添加一个阴影路径,相当于提前告诉GPU这个阴影的几何形状,这样阴影就可以独立渲染。
光栅化(shouldRasterize)
光栅化是一种缓存机制,开启后会缓存这个图片的bitmap,如果对应的layer和sublayers没有发生变化,就可以直接使用缓存而不用GPU再进行渲染,从而提高性能。
注意:
光栅化只能缓存100ms,而且只能存储屏幕大小2.5倍的数据,缓存空间十分有限。
组不透明(allowsGroupOpacity)
alpha并不是分别应用到每一层上,而是整个layer图层树完成之后,再统一加上alpha,然后和底下其他像素进行融合。
注意:
iOS7之后allowsGroupOpacity默认为YES,这样做的原因是为了保持子视图和父视图保持同样的透明度
- 当视图上有其他子视图
- 视图View的alpha值在0 ~ 1之间
- 视图view.layer.allowsGroupOpacity = YES
总结
离屏渲染的处理仅仅是我们日常所关注的性能中的其中一个点,在处理的时候也要根据具体场景具体分析,要注意并不是所有的离屏渲染都是必须要去避免的,开辟额外的帧缓存虽然有一定的性能损耗,但是保存渲染结果并进行最终的视图显示也是为了保持视图的流畅性。