Android 显示刷新机制、VSYNC和三重缓存机制
为了理解 APP 是如何进行渲染的,我们就必须了解手机硬件是如何工作的,也必须理解什么是 VSYNC。
首先,我们需要了解2个相关概念:
- 刷新率(Refresh Rate):代表了屏幕在一秒内刷新屏幕的次数,这取决于硬件的固定参数,例如 60Hz。
- 帧率(Frame Rate):代表了 GPU 在一秒内绘制操作的帧数,例如 30fps,60fps。
GPU 会获取图形数据进行渲染,然后硬件负责把渲染后的内容呈现到屏幕上,他们两者不停的进行协作。
如果刷新率和帧率,各自做自己的事,不相互协调工作,那么刷新频率和帧率并不总能够保持相同的节奏。如果发生帧率与刷新频率不一致的情况,就会容易出现画面撕裂(Tearing)的现象,也就是画面上下两部分显示内容发生断裂,来自不同的两帧数据发生重叠。
为了解决 Tearing 问题,Android 引入了 VSYNC 信号以及双重与三重缓存机制。
Android 黄油计划
从 Android 4.1 开始,谷歌致力于解决 Android 系统中最饱受诟病的一个问题,滑动不如 iOS 流畅。因谷歌在 4.1 版本引入了一个重大的改进—Project Butter,也即是黄油计划。
Project Butter 对 Android Display 系统进行了重构,引入了三个核心元素,即 VSYNC、Triple Buffer 和 Choreographer。Choreographer 在之前的文章《从源码分析Choreographer是如何实现VSYNC信号的请求及帧的刷新处理?(Android Q)》中已经分析过了,三重缓存机制我们后面介绍,这里我们重点讲解 VSYNC 的作用。
VSYNC(Vertical Synchronization)是一个相当古老的概念,对于游戏玩家,它有一个更加大名鼎鼎的中文名字—-垂直同步。垂直同步(vsync)指的是显卡的输出帧数和屏幕的垂直刷新率相同。在当下,垂直同步的含义我们可以理解为,使得显卡生成帧的速度和屏幕刷新的速度的保持一致。举例来说,如果屏幕的刷新率为 60Hz,那么生成帧的速度就应该被固定在 16ms。
上文中,我们已经知道了什么事画面撕裂(Tearing)现象以及它产生的原因,而 VSYNC 最重要的作用是防止出现画面撕裂。
VSYNC 信号是由屏幕(显示设备)产生的,并且以 60fps 的固定频率发送给 Android 系统,Android 系统中的 SurfaceFlinger 接收发送的 VSYNC 信号。VSYNC 信号表明可对屏幕进行刷新而不会产生撕裂。当 SurfaceFlinger 接收到 VSYNC 信号后,SurfaceFlinger 会遍历其层列表,以查找新的缓冲区。如果 SurfaceFlinger 找到新的缓冲区,SurfaceFlinger 会获取缓冲区;否则,SurfaceFlinger 会继续使用上一次获取的那个缓冲区。SurfaceFlinger 必须始终显示内容,因此它会保留一个缓冲区。如果在某个层上没有提交缓冲区,则该层会被忽略。
通常来说,帧率超过刷新频率只是一种理想的状况,在超过 60fps 的情况下,GPU 所产生的帧数据会因为等待 VSYNC 的刷新信息而被 Hold 住,这样能够保持每次刷新都有实际的新的数据可以显示。但是我们遇到更多的情况是帧率小于刷新频率。
在这种情况下,某些帧显示的画面内容就会与上一帧的画面相同。糟糕的事情是,帧率从超过 60fps 突然掉到 60fps 以下,这样就会发生 LAG,JANK,HITCHING 等卡顿掉帧的不顺滑的情况。这也是用户感受不好的原因所在。
接下来,我们以具体示例来看 VSYNC 的作用。
没有使用 VSYNC 时
我们来看没有 VSYNC 的情况:
这个图中有三个元素,Display 是显示屏幕,GPU 和 CPU 负责渲染帧数据,每个帧以方框表示,并以数字进行编号,如0、1、2等等。
- CPU 正常执行帧1,GPU 正常渲染帧1,所以帧1正常显示。
- 但,CPU 由于被占用等原因,等到即将显示帧2时,它才开始处理第二帧的内容,这显然完不成了,所以等到第二帧显示的时候,只能使用上一帧的内容显示了,也即是丢帧了。
上面丢帧的原因,我们可以从图中看出,是因为新的一帧开始的时候,CPU 在处理其他任务,并没有马上执行下一帧的任务,那么如何让 CPU 在新的一帧开始的时候立即处理显示内容呢?答案就在 VSYNC 身上!
使用 VSYNC 信号
我们来看,Android 引入 VSYNC 之后的帧执行示意图:
- 第0帧显示时,CPU 和 GPU 准备好了第一帧的内容。
- 第1帧刚开始显示时,CPU 放下手中的任务,立马处理第2帧显示相关的任务(这里使用了消息屏障机制,可以参考前文《Android消息循环的同步屏障机制及UI渲染性能的提升(Android Q)》),这样,在第二帧显示之前, CPU 和 GPU 也提前完成了显示任务的处理,第二帧正常显示。
可以看到,使用 VSYNC 信号机制,提升了渲染任务的优先级,优化了渲染性能,可有效的减少了丢帧、卡顿等问题。
但是上图中仍然存在一个问题:CPU 和 GPU 处理数据的速度似乎都能在 16ms 内完成,而且还有时间空余,也就是说,CPU 和 GPU 的帧率要高于 Display 的帧率。由于 CPU/GPU 只在收到 VSYNC 时才开始数据处理,故它们的帧率被拉低到与 Display 相同。但这种处理并没有什么问题,因为 Android 设备的 Display FPS 一般是 60,其对应的显示效果非常平滑。
但如果 CPU/GPU 的帧率小于 Display 的帧率,情况又不同了,将会发生如下图的情况:
在第二个 16ms 时间段,Display 本应显示 B 帧,但却因为 GPU 还在处理 B 帧,导致 A 帧被重复显示。
同理,在第二个 16ms 时间段内,CPU 无所事事,因为 A Buffer 被 Display 在使用。B Buffer 被 GPU 在使用。注意,一旦过了 VSYNC 时间点,CPU 就不能被触发以处理绘制工作了。
以上是使用双重缓存机制时产生的问题,那么又如何来解决呢?
为了解决这个问题,Android 引入了 Triple Buffer 机制。
三重缓存机制(Triple Buffer)
一般我们在绘制 UI 的时候,都会采用一种称为“双缓存”的技术(例如,上面几个例子)。双缓存意味着要使用两个缓存区,其中一个称为 Front Buffer,另外一个称为 Back Buffer。UI 总是先在 Back Buffer 中绘制,然后再和 Front Buffer 交换,渲染到显示设备中。理想情况下,这样一个刷新会在 16ms 内完成,下图就是描述的这样一个刷新过程:Display 处理前 Front Buffer,CPU、GPU 处理 Back Buffer。
只有两个 Buffer(Android 4.1之前)时,CPU 在空闲时,如果 Back Buffer 被占用了,它也只能等待 GPU 使用之后再次进行写入。我们可以想想,如果有第三个 Buffer 的存在,CPU 是不是就可以提前工作,而不至于空闲了?所以,Google 在 Android4.1 以后,引入了三重缓存机制:Tripple Buffer。Tripple Buffer 利用 CPU/GPU 的空闲等待时间提前准备好数据,并不一定会使用。
引入 Triple Buffer 效果如下图所示:
上图中,第二个 16ms 时间段,CPU 使用 C Buffer 绘图。虽然还是会多显示 A 帧一次,但后续显示就比较顺畅了。
那么,是不是 Buffer 越多越好呢?回答是否定的。由上图可知,在第二个时间段内,CPU 绘制的第 C 帧数据要到第四个 16ms 才能显示,这比双 Buffer 情况多了 16ms 延迟,并且大量的缓存数据也会导致内存增大,以及显示数据是否失效等问题。所以,Buffer 三个足矣。
总结
- 刷新率(Refresh Rate):代表了屏幕在一秒内刷新屏幕的次数,这取决于硬件的固定参数,例如 60Hz。
- 帧率(Frame Rate):代表了 GPU 在一秒内绘制操作的帧数,例如 30fps,60fps。
- GPU 会获取图形数据进行渲染,然后硬件负责把渲染后的内容呈现到屏幕上,他们两者不停的进行协作。
- 如果刷新率和帧率,各自做自己的事,不相互协调工作,那么刷新频率和帧率并不总能够保持相同的节奏。如果发生帧率与刷新频率不一致的情况,就会容易出现画面撕裂(Tearing)的现象。
- 从 Android 4.1 开始,谷歌在黄油计划中,引入了了三个核心元素,即 VSYNC、Triple Buffer 和 Choreographer。
- VSYNC 信号是由屏幕(显示设备)产生的,并且以 60fps 的固定频率发送给 Android 系统,Android 系统中的 SurfaceFlinger 接收发送的 VSYNC 信号。VSYNC 信号表明可对屏幕进行刷新而不会产生撕裂。
- 使用 VSYNC 信号机制,提升了渲染任务的优先级,优化了渲染性能,可有效的减少了丢帧、卡顿等问题。
- 三重缓存机制(Triple Buffer) 利用 CPU/GPU 的空闲等待时间提前准备好数据,有效的提升了渲染性能。