图像显示过程
屏幕显示图像需要一个过程,CPU进行视图的创建、布局计算、图片解码、文本绘制,然后交与GPU进行变换、合成、渲染,将结果保存到帧缓存区,再由视频控制器读取帧缓存区信息,即位图,再经过数模转换(数字信号转模拟信号)后逐行扫描进行显示(从左上角开始)。整个过程是由CPU和GPU进行协作完成。
假如屏幕刷新频率是每秒钟60帧,理想情况下,1/60s内完成一次渲染、更新帧缓存区、扫描显示。但是如果在1/60s内没有完成渲染更新缓存区,在逐行扫描显示的还是旧帧的位图数据,在扫描显示过程中新的一帧渲染完成后又更新了帧缓存区,那么就会在接下来的扫描显示新帧的位图数据,看到的效果就是只有下半部分在变,这种现象称之为撕裂。
双缓冲机制与垂直同步
苹果在图像显示流程中一直采用垂直同步+双缓冲区
机制。在双缓冲机制下,GPU 会预先渲染好一帧放入一个缓冲区内(前帧缓存),让视频控制器读取,当下一帧渲染好后GPU会直接把视频控制器的指针指向第二个缓冲区(后帧缓存),但是如果上一帧仍在读取中进行了切换,则后续读取的变成了第二个缓冲区的位图,产生图像上下不是同一帧的现象(撕裂)。为了解决这种现象,GPU采用了垂直同步机制,当开启垂直同步后,GPU会等待显示器的VSync信号发出后才切换新指针并开始新一帧的计算与渲染,并会等待下一次VSync信号到来将结果显示到屏幕。
由于开启了垂直同步机制,那么如果在两个VSync信号之间CPU与GPU没有完成这一帧的处理,那么这一帧将会被丢弃,而屏幕上显示的仍然为前一帧的内容,这就是掉帧。
说明:为了把显示器的显示过程和系统的视频控制器进行同步,显示器会由硬件时钟产生一系列的信号,在逐行扫描过程中,每一行结束会发出一个水平同步信号,简称HSync信号,当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号,简称VSync信号。显示器通常是固定频率进行刷新,这个频率即是VSync信号产生的频率。
卡顿检测与优化
当掉帧产生时,会感觉到界面的卡顿,卡顿引起的原因有很多,因此需要通过一些检测手段来定位并将堆栈信息保存用于分析,目前有很多检测手段主要归为两种,一种是通过CADisplayLink,另外一种是通过RunLoop监听或定时ping主线程。像YYKit、Matrix等开源项目。优化推荐阅读YY大神的博客iOS 保持界面流畅的技巧
离屏渲染
离屏渲染产生的根本原因,当GPU在渲染时会按照“画家算法”将图层按次序输出到帧缓存区,在绘制之后就会将其丢弃,从而节省空间,假如某个图层的会影响到其他图层的效果,那么就不能绘制完即丢弃,因此就需要开辟一块空间保存中间状态,以在后续图层绘制时取出并再次进行修复、裁剪等,这块被开辟的空间称为离屏缓冲区,借助离屏缓冲区进行渲染被称为离屏渲染。
常见的离屏渲染包括有以下几种情况:
- 使用了蒙版图层效果layer.mask
- 为layer设置了圆角,并且需要进行裁剪且影响子图层layer.masksToBounds
- 设置组透明度,layer.allowsGroupOpacity=YES;layer.opacity < 1
- 为layer添加了投影
- 使用了光栅化layer.shouldRasterize=YES
- 绘制了文字的layer(UILabel, CATextLayer, Core Text 等)
- UIBlurEffect,同样无法通过一次遍历完成
在触发离屏渲染时会进行离屏缓冲区与帧缓冲区的切换,上下文的切换会大量消耗性能,因此我们尽量避免。尽管离屏渲染开销很大,但是当我们无法避免它的时候,可以想办法把性能影响降到最低。
CALayer为这个方案提供了对应的解法:shouldRasterize。一旦被设置为true,Render Server就会强制把layer的渲染结果(包括其子layer,以及圆角、阴影、group opacity等等)保存在一块内存中,这样一来在下一帧仍然可以被复用,而不会再次触发离屏渲染。有几个需要注意的点:
- 离屏渲染区空间大小不超过屏幕总像素的2.5倍
- 缓存时间超过100ms没有被使用,会自动被丢弃
- layer的内容(包括子layer)必须是静态的,一旦发生变化那么缓存也就失去了作用
- 如果layer的子结构非常复杂,渲染一次所需时间较长,同样可以打开这个开关,把layer绘制到一块缓存,然后复用这个结果,而不需要每次都重新绘制整个layer树