Android 屏幕刷新机制 VSync+Choreographer★

1.显示系统基础知识

显示系统一般包括CPU、GPU、Display三部分,其中CPU负责计算帧数据,然后把计算好的数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到图像缓冲区buffet里存起来,然后Display(屏幕或显示器)负责把buffer里的数据呈现到屏幕上。

这里涉及几个基础概念:

①屏幕刷新频率

屏幕刷新频率是指一秒内屏幕刷新的次数,即一秒内显示了多少帧图像,单位是赫兹Hz。常见的屏幕刷新频率是60Hz。

注:刷新频率取决于硬件的固定参数,不会变的。

②逐行扫描

显示器并不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点,这一过程快到人眼无法察觉到变化。

比如常见的刷新率为60 Hz的屏幕,这一过程只需要1000 / 60 ≈ 16ms。

③帧率Frame Rate

帧率指GPU在一秒内绘制操作的帧数,单位fps。Android系统采用60fps,即每秒钟GPU最多绘制60帧画面。

帧率是动态变化的,例如当画面静止时,GPU是没有绘制操作的,屏幕刷新的还是buffer中的数据,即GPU最后操作的帧数据。

④画面撕裂tearing

指一个屏幕内的数据来自2个不同的帧,导致画面出现了撕裂感。

比如屏幕更新到一半的时候用户进程更新了Frame Buffer中的数据,这将导致屏幕上画面的上部分是前一帧的画面,下半部分变成了新的画面,于是就会让用户觉得画面有闪烁感。

Android 屏幕刷新机制 VSync+Choreographer★_第1张图片

解释一下画面撕裂的原因:

单缓冲区时,图像绘制和屏幕读取操作使用的是同一个Frame Buffer,导致屏幕刷新时可能读取到的是不完整的一帧画面。

屏幕刷新频率是固定的,比如每16.6ms从buffer中取出数据显示完一帧,理想情况下帧率和刷新频率保持一致,即每绘制完成一帧,显示器就显示一帧。但由于CPU/GPU写数据是不可控的,可能会出现buffer里有些数据根本没显示出来就被重写了,即buffer里的数据可能是来自不同的帧, 当屏幕刷新时,它并不知道buffer的状态,因此从buffer抓取的帧并不是完整的一帧画面,也就出现了画面撕裂。简单说就是因为单缓冲区时图像绘制和屏幕读取操作使用的是同一个Frame Buffer,造成Display在显示的过程中,buffer内的数据可能会被CPU/GPU修改,因而出现了画面撕裂。

为了解决画面撕裂问题,出现了双缓存。

 

2.双缓存机制+Vsync

双缓存机制就是让绘制和显示拥有各自的buffer,CPU和GPU共用后缓冲,它们轮流使用后缓冲,而Display单独使用前缓冲。CPU计算数据后交给GPU渲染,然后GPU将完成的一帧图像数据写入到后缓冲中,显示器每次刷新屏幕时都从前缓冲中取数据。这样当屏幕刷新时,前缓冲中的数据并不会发生变化,只有当后缓冲数据准备就绪后,它们才进行交换。如下图:

Android 屏幕刷新机制 VSync+Choreographer★_第2张图片

注意:

虽然引入了双缓存,但如果切换的时间点不对,比如在画面更新到一半时切换,还是不可避免地产生画面闪烁的异常。那两个buffer的内容什么时候进行交换呢?假如是后缓冲区准备完成一帧数据后就进行交换,那如果此时屏幕还没有完整显示上一帧内容的话,肯定会出问题。所以只能等到屏幕显示完一帧数据后才可以执行交换操作。

屏幕刷新时,当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次循环,此时有一段时间空隙,称为Vertical Blanking Interval(VBI)。这个时间点就是进行缓冲区交换的最佳时间了,因为此时屏幕没有在刷新,也就避免了交换过程中出现画面撕裂的状况。

Android引入VSync机制来确定缓冲交换时间,VSync就是利用VBI时期出现的垂直同步脉冲vertical sync pulse来保证双缓冲在最佳时间点进行交换(是指各自的内存地址,可以认为该操作是瞬间完成)。也就是由底层模拟VSync信号一直固定发出,当用户进程接到Vsync信号时就开始渲染处理,当VSync信号到来且缓冲区数据准备完毕后,就会进行缓存交换。

注意:VSync不仅控制了后缓冲区和前缓冲区的数据交换,还控制了CPU何时开始进行绘制计算。

那么有人就会说:真正解决画面撕裂问题的是VSync,而不是双缓冲,那是不是不要双缓冲只要VSync也可以?

假设只有VSync,比如现在屏幕正在渲染数据,而CPU在等VSync信号,屏幕将数据渲染完毕后,发送VSync信号,CPU收到信号后就去计算数据,计算完后才会写入帧缓冲,那么在CPU计算数据这段时间内屏幕干什么呢?它只能接着刷新帧缓冲区的数据,由于CPU还没有将新数据计算完毕刷入前缓冲,所以显示的还是上一帧的数据,这样就会卡顿。

所以,有双缓冲的情况下,CPU使用后缓冲计算数据,屏幕使用前缓冲渲染数据,两者可以同时工作,计算一个就可以渲染一个,典型的生产者消费者模式,只不过使用VSync信号来进行数据的交换;而没有双缓冲的情况下,两者需要排队使用帧缓冲,不能同时工作,就变成了我等着你计算,你计算完了等着我渲染,VSync此时的作用就是进行排队,这样会大大增加卡顿。因此,VSync真正解决了撕裂问题,而双缓冲优化了卡顿问题。

 

3.Android屏幕刷新机制

首先大致说一下Android屏幕绘制流程:

①任何一个View都是依附于Window的,一个Window对应一个Surface;

②View的measure、layout、draw等均是计算数据,这些是CPU干的事。CPU把这些事干好后,再经过一系列计算将数据转交给GPU;

③GPU将数据栅格化后,就交给SurfeceFlinger;

④SurfeceFlinger将多个Surface数据合并处理后,就放入后缓冲区;

⑤屏幕以固定频率从前缓冲区拿出数据渲染,渲染完毕后发送VSync,此时前后缓冲区数据交换,屏幕绘制下一帧。

这是建立在开启硬件加速的情况下的(Android4.0默认开启了硬件加速),如果没有硬件加速,就需要去掉GPU部分,可以简单理解为CPU直接将数据转交给SurfeceFlinger。

从这个过程可以看出数据的传递流程:CPU -> GPU -> Display,而且CPU和GPU是排队工作的(它俩共用一个缓冲区),它俩和屏幕并行工作(屏幕单独用一个缓冲区)。

现在分不同情况详细分析一下屏幕绘制流程:

1)没有VSync的绘制过程

在Android4.1之前,屏幕刷新遵循双缓存机制。

Android 屏幕刷新机制 VSync+Choreographer★_第3张图片

Display一行看作是前缓冲,GPU和CPU两行叠加起来看作是后缓冲(因为它俩排队使用),将VSync线隔离开的竖行看作一个帧。Display表示显示设备,上面的数字表示图像帧序号,GPU方块表示GPU正在准备数据,CPU方块表示CPU正在准备数据。

以时间顺序看一下整个过程:

①在第一个时间周期(即两个VSync之间的间隔)Display正常渲染第0帧画面,此时CPU已经开始准备第1帧数据,GPU正好在CPU准备好后开始处理且在第一个周期内完成;

②因为第1帧数据渲染及时,Display在第0帧显示完成后,缓存进行交换,因此Display正常显示第1帧。但是第2帧开始处理,CPU/GPU并不是在Vsyn一到来时就立马进行处理,而是由于某种原因CPU处理第2帧数据比较晚,GPU在CPU处理完成后再去处理,导致GPU处理完成时已经超过了第二个时间周期。

③由于Display刷新率是固定的,第2个VSync来时,第2帧数据还没有准备就绪,缓存就无法进行交换,所以显示的还是第1帧,就产生了Jank丢帧现象(丢帧、掉帧表示这一帧延迟显示)。

④当第2帧数据准备完成后,它并不能马上被显示,而是要等待下一个VSync到来时才进行缓存交换再显示。

所以,整个过程中屏幕多显示了一次第1帧。直接原因就是第2帧的CPU/GPU计算没能在VSync信号到来前完成 。究其根本会发现第2帧CPU/GPU计算不是在第1个Vsyn一到来时就立马进行处理,而是直到第2个VSync快来前才开始处理,这样就导致第2帧的CPU/GPU计算没能在第2个VSync信号到来前完成计算。

由于双缓存的交换是在Vsyn到来时进行的,交换后屏幕会取Frame buffer内的新数据,此时后缓冲区就可以立即供CPU、GPU准备下一帧数据了。所以,如果Vsync一到来时CPU/GPU立马就开始操作的话,是有完整的16.6ms的,这样基本会避免jank的出现了(除非CPU/GPU计算超过了16.6ms)。

那如何让CPU/GPU计算在Vsyn一到来时就进行呢?Android 4.1引入了黄油计划(VSYNC),上层开始接收VSYNC(Choreographer),并且加入了三缓冲。

2)drawing with VSync

为了优化显示性能,Google在Android 4.1系统中对Display系统进行了重构,实现了Project Butter(黄油工程):系统在收到VSync pulse后,将马上开始下一帧的渲染。即一旦收到VSync通知(16ms触发一次),CPU和GPU就立刻开始计算然后把数据写入buffer。

Android 屏幕刷新机制 VSync+Choreographer★_第4张图片

有了VSync后,CPU总是在指定的地方开始。即CPU/GPU会根据VSYNC信号同步处理数据,这样就可以让CPU/GPU有完整的16ms时间来处理数据,减少了jank。

但是这样并不能完全避免jank问题,只能优化卡顿。比如界面比较复杂,CPU/GPU的处理时间较长,超过了16.6ms:

Android 屏幕刷新机制 VSync+Choreographer★_第5张图片

由于CPU/GPU处理耗时过长,超过了16.7ms,当第一个VSync信号到来时,缓冲区B中的数据还未准备完毕,Display未能完成交换只能继续显示缓冲区A中的内容。此时缓冲区A、B分别被Display和GPU占用了,CPU在第二个VSync时无法开始准备下一帧数据而只能空闲运行,当下一个VSync信号来临时,Display与B完成缓冲区交换,CPU才能继续处理下一帧数据,导致的结果就是相当于把屏幕的刷新率降低了。因为在第一个周期时原本应该显示第二帧的又多显示了第一帧。究其原因就是因为两个Buffer都被占用,CPU无法准备下一帧的数据。

分析一下整个过程:

①在第一帧里,GPU墨迹了半天没有搞完,直到第二帧里GPU还在处理B帧,导致缓存没能交换,因此Display显示的还是第一帧的数据,即A帧被重复显示,出现了jank卡顿。

并且会发现在第一个VSync信号过来后,CPU什么都没做,这是因为GPU占着后缓冲(那个绿色的长B块),当B帧完成后,又因为缺乏VSync pulse信号

你可能感兴趣的:(android)