Floating Cat

Android图形显示系统基本原理

字数统计: 5k阅读时长: 17 min
2017/09/12 Share

计算机图形显示系统

人眼与帧率

要理解应用流程度,我们首先引入FPS这个概念.FPS是Frames Per Second,它描述的是GPU在一秒内能够渲染出静态画面的数量(一张静态图片称之为一帧),通俗点讲就是GPU每秒钟能画出多少画面.FPS是衡量GPU性能的一个重要指标,通常来说性能越强的显卡,一秒内能够渲染出静态画面的数量越多,给我们的视觉效果越好.

解释完帧率之后,我们还要具备一点生物学的知识,即需要了解视觉暂留现象.由于人眼特殊的生理结构,如果所看的画面以每秒10~12帧的速度播放时,此时我们就认为这些图片时连贯的,这种现象就是视觉暂留.在视觉暂留的基础上,如果我把一张一张的静态图像按顺序以一定的速度出现在我们眼前,此时我们大脑就会认为画面中的物体是在运动的,这就是典型动画的原理.

屏幕刷新率与VSync

FPS是描述GPU的一个重要指标,但GPU画出来的图像需要借助显示器来显示出来才能被我们所感知.与帧率相对应,显示器也有一个指标用来描述一秒内从GPU取出画面的的数量,我们称之为刷新率.

屏幕刷新率(Refresh Rate)是指屏幕每秒钟更新画面的次数,对于特定设备而言它是个常量,单位Hz.为了方便,我们采用一张图来描述其关系:

image-20181127192131031

由于帧率和刷新率分别是用于描述不同设备的指标,而这两个指标因设备不同而有所差异,换句话说这两者并不总是能保持相同的节奏,根据情况,可以分为以下三种:

  • 帧率和刷新率相等
  • 帧率大于刷新率
  • 帧率小于刷新率

在帧率和刷新率相等的情况,GPU每画出一帧,显示器就显示一帧,但实际情况却是由于硬件不同或需要渲染的界面复杂度问题,我们很难保证帧率和刷新率一定是相等的.实际上,更多的是遇到除此之外的情况,即后两者.但其余两种情况都会或多或少的产生问题.

帧率小于刷新率

在帧率小于刷新率的情况下,比如帧率是30fps,显示器刷新率是60HZ,此时我们一秒钟内看到屏幕内的画面还是更新了60次,只不过其中一些更新画面是没有变化的而已,因此该情况下由显卡输出的一张图片(一帧)实际上在显示器期内被刷新了两次而已,这种现象带给我们的就是卡顿感.

image-20181128103849709

帧率大于刷新率

那对于帧率大于刷新率的情况又是如何呢?比如帧率是75fps,刷新率是100Hz,这意味在显示器更新画面的时间里,GPU描画了1+1/3的画面.这样在画面显示的时候,那个1/3的画面就会覆盖那个完整画面上部的1/3,在下次的图像刷新的时候,GPU会描画剩下来得2/3和新的2/3的画面.因为屏幕的更新只能跟上画面更新的2/3,这样图像的上部的1/3或是下部的1/3就会和剩下的画面合不上,以屏幕快速显示数字为例,在帧率和屏幕显示率一致的情况下,其效果如下:

image-20181128101842907

但在帧率大于刷新率的情况下可能出现以下现象:

image-20181128101938613

对于像上图所示的现象,我们称之为画面撕裂,即一个画面上出现了两帧画面的内容.如何解决画面撕裂问题呢?在了解其解决方案之前,得先来认识Vsync.

显卡与VSync

CRT显示器与VSync

VSync(Verti Synchronization)在计算机显示系统领域是一个非常古老的名词了,通常我们将它翻译成垂直同步.VSync是显卡的一项功能,用来限制GPU绘制的帧数,使其与显示器在一秒内刷新的次数相匹配,也就是指显卡的输出帧数和屏幕的垂直刷新率相等.其实VSync本来只是CRT显示器上的概念,CRT即我们常说的模拟显示器,其显示原理是通过电子枪扫描荧光屏来显示图像的.其扫描过程是从左到右,从上到下逐行刷新的过程,前者称之为水平刷新,后者称之为垂直刷新,其流程大概如下所示:

image-20181127174542323

假设我们现在有一个4*4的图片要显示在4*4的显示器上(单位是像素):

A1 A2 A3 A4
B1 B2 B3 B4
C1 C2 C3 C4
D1 D2 D3 D4

当显示器要电子枪扫描到最后一个像素D4时,显卡会发出一个VSync信号,来通知显示器已经完成一帧画面的扫描,需要回到A1开始进行下一帧的扫描.总之,这个VSync信号由显卡发出,一方面用来告诉显示器需要回到A1位置,另一方面通知显卡准备输出下一帧画面.

在LCD显示器中已经没有垂直扫描这回事了,因此VSync名字本身已经没有意义了.但是LCD仍然需要VSync信号,不然显卡就无法知道在什么时候才可以输出下一帧画面,显示器也无法知道什么时候可以开始处理一帧画面.因此这个VSync这个名称就这样沿袭下来.

现在,我们只需要记住VSync就是用来保持显卡生成帧的速度和屏幕刷新的速度一致的存在,即帧率和刷新率.比如,如果屏幕的刷新率为60Hz,那么生成显卡生成一帧画面的时间就应该固定在(1/60)s,即60fps.

显卡工作流程

抛开具体实现不说,显卡的工作流程还是比较清晰的,简单点说它接受来自CPU和内存的数据,经过处理之后,生成一帧图片.循环往复,继续生成下一帧的图片,下图揭示了其工作流程:

image-20181128152242597

在上图中,需要重点注意帧缓冲器,其本质就是显存中划出来的一块区域,用于存储一帧图片每个像素点的数据,显卡会将该区域的数据依次输出到显示器中,当全部输出完毕后,会发出一个VSync信号到显示器内,如此往复.实际上,后面垂直同步,以及后面要提到双重缓存,三种缓存本质上描述的就是帧缓冲器数据输出规则.

通过上图不难发现该区域的数据是由显卡的光栅操作单元生成的,此外缓冲区采取的更新策略是”新数据覆盖老数据的”,这意味着在实际数据处理过程中,缓冲区内的数据如果没有及时被输出到显示器,该区域内的部分数据可能会被由光栅操作单元生成新数据覆盖.举个例子,假设当前帧缓冲区内已经有一帧完整的图片A,此时显卡生成了下一帧的画面B,并准备写入帧缓冲区,在写到一半的时候,收到了VSync信号,这时候缓冲区的数据被输出到显示器.糟糕的是此时缓冲区的数据是一半是A画面一半是B画面,因此我们会在显示器上由两张图片拼接出来的画面,这就是画面撕裂现象的原因,之前数字4显示不全同理.

那该如何解决该问题呢?分析上面问题的根源在于写缓冲区的的操作和VSync信号到来时机没有同步导致,如果我们让他们同步起来会如何呢?回想下我们是如何利用生产者-消费者解决同步问题的.现在我们来定义这么一条规则就是:当显卡生成一帧完整的图片并写入帧缓冲区后,停下来等待VSync信号的到来,接下来在继续渲染下一帧图片并写入缓冲区.这条规则能够保证帧缓冲区内始终是一帧完整图片的数据,就不会出现画面撕裂现象了.

11111111

双缓冲机制

从性能角度出发,如果只对一块缓冲区进行读写无疑效率比较低下:一方面屏幕要从该区域去读,另一方面显卡要等待去写.因此在实际中,其实帧缓冲区实则被划分为两部分:

  • 前缓冲区: 用来缓存要显示到屏幕的帧数据
  • 后缓冲区: 用来缓存显卡生成的帧数据.

屏幕只能前缓冲区读取数据用于显示,显卡只能往后缓冲区写入新生成的帧数据.需要注意的是两块缓冲区并不发生实际上的数据拷贝操作,即将后缓冲区的帧数据拷贝到前缓冲区,而是在前缓冲区的帧数据已经推到屏幕上,且新的帧数据被写入到后台缓冲区后,进行指针交换操作,将原来的后缓冲区变为前缓冲区.

image-20181128171645499

三缓冲机制

三缓冲机制是在双缓冲机制基础上发展而来,其目的是在发生卡顿时能够充分利用CPU资源,同时保证尽可能快的从卡顿现象恢复成流畅状态.更具体的解释见下文.

Android显示系统

在Android体系架构中,通常我们采用XML进行布局描述,CPU会对抽象的XML布局内容进行Measure -> Layout -> Draw操作,然后将其内容计算成Polygons(多边形)或Texture(纹理),GPU会对Polygons或Texture进行Rasterization(栅格化)操作,Rasterization后的数据会被写入到帧缓冲区等待显示器显示.下图描述了上述过程:

image-20181128173844650和计算机显示

此外计算图形显示系统一样,Android显示系统同样会遇到屏幕刷新率和帧率一致的情况:

  • 帧率小于屏幕刷新率: 可能会导致卡顿现象
  • 帧率大于屏幕刷新率: 可能会导致画面撕裂现象

对于该问题,Android采用之前计算机系统一致的方案来解决.为了更好的了解Android是如何解决赶问题的,首先要对Android显示原理有所了解.

Android显示原理

从开发者的角度出发,Google抽象出View概念,因此在开发过程中我们只需要重点关注View的样子,而无需去了解图形系统底层知识.对于View而言,Activity只是Google我们抽象出来的View控制器,以便我们在此可以对View进行一些控制,比如控制View被点击后的行为,那View真正的载体是什么?

在Android中,View真正的载体是Window,即窗口(其实用视窗系统来描述Android显示系统更形象),每一个Window都包含了各自想要显示内容,不同的Window之间有层级关系,即Z-Order,用来描述Window的显示次序,其本质就是在二维坐标系x-y添加了Z轴,其方向为垂直于屏幕表面指向屏幕外.

image-20181128180954524

每个Window对应于一个Surface,Surface内部含有一块可供”涂鸦”的画布Canvas.在Android系统提供了WindowManagerService服务用于管理系统中所有的Window.当一个应用需要渲染UI时,WindowManagerService服务会为其创建描述其Window信息的WindowState对象,然后通过SurfaceFinger服务将需要显示的多个Surface按照Z-Order次序混合输出到FrameBuffer(帧缓冲区),接下来就是等待VSync信号到来,再显示在屏幕上.

Android之Project Butter

在Android 4.1之前,界面卡顿是Android中最受诟病的一点.为了解决界面卡顿问题,Google为Android引入了Project Butter计划,即常说的黄油计划.在该项目中,Google对Android显示系统进行重构,并引入了三个至关重要的改进:

  • VSync增强: VSync不仅仅用于避免画面撕裂现象,现在它还会通知GPU在渲染下一帧之前要等待屏幕完成逐行绘制.
  • Triple Buffer: 三缓冲机制
  • Choreographer: 用于协同nimations,input和drawing一起工作

需要注意的是Android中一直存在VSync机制,只不过早期VSync只是为了避免画面撕裂(screen tearing)现象.为了更好的了解增强过的VSync和Triple Buffer,我们先来看一下早期图像显示的过程,即VSync只用来避免画面撕裂的情况:

image-20181128230745131

在上图中,横轴表示时间,纵轴表示Buffer的使用者,对于GPU和CPU一行中的长方形其代表的是帧缓冲,其宽度可以认为是处理该帧所需要的时长,而长方形中数字代表当前帧数.此外两个VSync信号之间间隔16.6ms.

我们从左往右开始看,开始时当前屏幕显示第0帧,CPU和GPU开始准备第1帧的数据(CPU计算第1帧的纹理后交给GPU进行栅格化),并及时计算完成,并等待下一个VSync信号后屏幕显示第1帧画面在显示第1帧画面时,CPU和GPU开始准备第2帧的数据,但由于某些原因导致系统缓慢或者画面太复杂导致第2帧数据没有在第二个VSync信号到来时准备好,显示器仍然显示理第1帧的画面.这种同一帧数据被显示多次的情况称之为”Jank”.

image-20181128235257542

不难发现早期的VSync尽管能够避免画面撕裂线程,但却无法避免Jank.

现在我们来看4.1之后,被增强过的VSync机制:GPU在渲染下一帧之前要等待屏幕完成逐行绘制,也就是每一帧处理都从接受到VSync信号开始,这样我们就可以充分利用这16.6ms.

image-20181128235621846

在上图中,时间从屏幕显示第0帧开始,CPU/GPU开始准备第1帧数据,一旦接受到下一个VSync信号,除了将原来准备好的第1帧数据显示出来,同时还会CPU/GPU还会开始准备第2帧数据…不难发现在这种情况下,CPU/GPU的工作速度和VSync保持一致,

除了增强的VSync机制,Google还采用三缓冲机制来帮助从卡段中快速恢复成流畅的状态.首先来看传统的双缓冲机制,理想情况下,其工作状态如下:

image-20181129001112405

在上图中,以GPU一行为例,长方形A和B分别代表两块缓冲区域,分别代表前台缓冲区和后台缓冲区.开始时A作为前台缓冲区,此时GPU会想后台缓冲区B写入帧数据;当B缓冲区准备就绪后,A,B交换,B变成前台缓冲区,A变成后台缓冲区(其交换原理并不是实质的数据拷贝和转移,详见之前).

从上图看起来,一切都很流程,CPU/GPU充分利用每个16.6ms,在前一个VSync到来时开始准备数据,并在后一个VSync到来时准备好数据.但问题时,我们无法确保CPU/GPU都能在16.6ms内能够准备好数据,如下所示:

image-20181129002236319

还是从左边开始看起,此时进入第一个16.6ms,当前显示前台缓冲区A中的帧画面,与此同时CPU/GPU开始准备下一帧的数据.糟糕的是在下一个VSync信号到来之前,GPU未能及时准备好数据,也就是没有及时把帧数据写入到后台缓冲区B中,这种情况下后台缓冲区数据未就绪,因此不能交换前台缓冲区A和后台缓冲区B,这种情况下,显示器只能继续使用前台缓冲区A中的数据,即显示和之前相同的帧,即发生Jank.

更糟糕的是,改进后的VSync要求每一帧数据处理必须要从接受到VSync信号开始,在这种GPU未能及时在下一个VSync到来前及时完成工作的情况,CPU在后一个16.6ms内只能处于空闲状态,也就是上图第2个16.6ms内CPU一直处在空闲状态,而不是进行下一帧的处理操作.如果CPU/GPU大部分时间内都无法完成一帧数据的处理,那么就将导致连续的卡顿现象.

既然Jank难以完全避免,那如果是否能在Jank发生时充分利用CPU而不是使其处在空闲状态呢?如果能,那就可以在发生Jank后,快速恢复成流畅的状态?Google为了解决该问题,引入的三缓冲机制,即在原来双缓冲的机制上加入了第三块缓冲区.

image-20181129004214237

如上图,以CPU一行为例,共存在A,B,C三个缓冲区.在第一个VSync信号到来时,尽管A和B缓冲区都在使用中,但CPU仍然可以使用第三个缓冲区C来生成帧数据.从上图也可以看出,整个过程就在开始时Jank了一次,后续都是流畅的.这就是三缓冲区帮助Android从卡顿现象中快速恢复成流畅状态的原理.

值得注意的是,Android系统并非一直都是启用三缓冲机制,多一个缓冲区意味着消耗更多的资源.此外一个缓冲区的帧数据要想被显示到屏幕上,最终要跨越两个VSync信号,这样会让用户感觉到延迟.

image-20181129005317770

VSync总结

现在关于VSync和Triple Buffer的作用已经明了,接下来总结下VSync相关的知识,首先需要知道的是在Android设备中存在两种VSync信号:

  • 由硬件VSync产生: 由硬件中断产生,是一个脉冲信号,类似CPU时钟
  • 由软件VSync产生: 在硬件不支持的情况下,通过软件模拟产生:SurfaceFlinger会模拟该信号,并通过Binder传给Choreographer,

简单描述下硬件是如何产生VSync信号:在Android启动过程中,init进程会启动SurfaceFinger进程,并执行main_surfaceflinger.cpp中的main()方法,在该方法的执行过程中会初始化HWComposer.而HWComposer就是基于硬件实现的VSync信号发生器,该信号用来通知SurfaceFlinger以便控制生成帧的速度.

Choreographer总结

Choreographer被设计用来接收VSync信号(通过注册DisplayEventReceiver来接受VSync信号)的Java类,正如含义一样,它的主要工作是用来指挥和协调动画,输入和绘制的时序.如果将显示过程当做一台舞蹈剧的话,那它无疑相当于编舞者了.在Choreographer中存在FrameCallback接口:

1
2
3
public interface FrameCallback {
public void doFrame(long frameTimeNanos);
}

在新一帧画面被渲染时会被会调用该接口的doFrame(long frameTimeNanos)方法,其参数frameTimeNanos代表该帧开始渲染时的时间.基于该接口,我们可以借助它来实现帧率检测功能.

总结

本次主要谈了计算机图形显示系统中一些概念,如帧率和屏幕刷新率,并重点分析了卡顿和画面撕裂现象的由来.此外进一步延伸到Android显示系统部分原理.

CATALOG
  1. 1. 计算机图形显示系统
    1. 1.1. 人眼与帧率
    2. 1.2. 屏幕刷新率与VSync
      1. 1.2.1. 帧率小于刷新率
      2. 1.2.2. 帧率大于刷新率
    3. 1.3. 显卡与VSync
      1. 1.3.1. CRT显示器与VSync
      2. 1.3.2. 显卡工作流程
      3. 1.3.3. 双缓冲机制
      4. 1.3.4. 三缓冲机制
  2. 2. Android显示系统
    1. 2.1. Android显示原理
    2. 2.2. Android之Project Butter
      1. 2.2.1. VSync总结
      2. 2.2.2. Choreographer总结
  3. 3. 总结