Android的图形之美(二)

上文我们提到,窗口绘图表面进行绘制的时候,会在画布上(非硬件加速如Canvas)的一个图形缓冲区中,然后SurfaceFlinger通过OpenGL图像块API来将这个图形缓冲区渲染到帧缓冲区中。我们对这个过程用图形来进行抽象分析。如下图,上层为生产,下层为消费,每一层有buffer为数据载体。其中,上层的buffer我们称为图形缓冲区、下层的buffer为帧缓冲区。

18582563-2005160f24690704

下层的帧缓冲区如下,显示屏上的内容是从帧缓冲区读取的,读取的属性从buffer的起始地址开始,从上往下读取,从左往右扫描整个buffer,将内容映射到显示屏上。若只在一个buffer中进行合成和读取,实际上这个buffer可能就不是包含当前帧的内容,因此,需要一个帧用于读取,一个帧用于合成,所以如下我们所说的双缓冲。前缓冲区为显示内容到屏幕的缓冲区,后缓冲区用于合成下个图形的帧缓冲区。实际上当如下所示的前置缓冲区渲染到屏幕之后,后缓冲区已经合成了,显示屏准备显示下一屏的时候,后帧缓冲区变为前缓冲区,前缓冲区变为后缓冲区。

18582563-fbd2c6468c70d5c0

前后更替进行渲染是在理想情况下进行的,既当前帧显示完毕,后一帧合成完毕是处于理想的状态下,实际上,可能后缓冲区合成还没开始或没合成完毕或合成很快,导致正在显示的一帧含有其他的,这样就造成看起来丢帧或撕裂(显示多帧)。这种情况和两个帧率有关。一个为HZ(屏幕刷新率),一个为FPS(系统帧率速率)。其中HZ和屏幕相关,一般我们常说的1秒展示60帧,既60HZ,既大概16ms刷新一帧,而FPS代表1秒内的合成帧数。那么这样我就可以知道了,若HZ>FPS,那么就会导致到屏幕一直刷新的仍然是相同的帧数,给人的感觉就是静止了一会儿,既卡顿;若HZ<FPS, 则合成的速率过快,缓冲区有限,导致把钱缓冲区的数据给后面的帧数据覆盖,造成一个屏幕有多帧的想象,造成撕裂。如何解决两个速率同步问题?

VSYNC——垂直同步,解决帧同步问题。当屏幕从缓冲区扫完一帧到屏幕上之后,开始扫下一秒之前,发出一个同步信号,该信号用来切换前缓冲区和后缓冲区。实际上,屏幕的速率是固定的,HZ固定,因此,需要配合的是FPS,既系统硬件需要配合,怎么配合呢?就是通过VSYNC来实现两者同步。

在分析垂直同步之前,先分析下SurfaceFlinger的作用——图形合成者。我们从上面的图中可以看出,SurfaceFlinger是上层的消费者,下层的生产者,其实一个服务。和众多AMS、WMS服务类似,在System_server启动的时候启动。

18582563-6e86c973495e3f69

如上图所示,上层应用程序我们看到的包括导航栏,通知栏、悬浮窗以及微信界面,都会对应到SurfaceFlinger一个个Surface对象,上层会进行绘制到Surface的Layer上,如下图所示

18582563-c4cf89d09329cc61

SurfaceFlinger将多个Surface进行合成之后放到后缓冲区,然后等待屏幕读取展示。那么Surface的内容从哪里来呢?结合上图微信界面和上文中分析的Activity启动的时候会进行窗口绘制表面绘制的分析,我们知道,所有内容在画布(画板)Canvas绘制之后(绘制到Surface(图层)中,对应SurfaceFlinger的Surface的Layer对象上)就会被SurfaceFlinger服务消费然后合成到帧缓冲区中。如下图所示,多个surface对应多个Buffer Queue(下面会讲到),每个surface都会产生buffer,多个surface最终经过SurfaceFlinger进入到硬件合成形成最终的画面。

18582563-bcb84d4462ed825b.png

我们在第一个图中已看到,生产者的数据也是以buffer为载体,那么对应的Surface实际上是一个多缓冲区,对应BufferQueue。那么第一个图的关系再继续深入之后就如下图所示:

18582563-09dc18bd79e20846

Surface内部提供一个BufferQueue,应用程序的绘图表面为上层,图形的提供者,既生产者(Producer),而消费者(Comsumer)既为SurfaceFlinger,用于合成生产者生产的图形数据。

其状态变化过程如下:

从BufferQueue转移到上层(Free->Dequeued),上层绘制完成回到BufferQueue中(Dequeued -> Queued), 接着SurfaceFlinger拿去合成(Queued -> Acquired),合成完成又放回队列中(Acquired -> Free)。

我们分析了生产者和消费者的模型、以及潜在的存在卡顿或裂屏的情况存在,那么Android系统解决这一问题的过程是怎样的呢?

在分析解决过程的时候,以硬件处理来分析,我们先区分哪些是在CPU中进行的,哪些又是在GPU进行的。在CPU进行的有包括应用窗口图形测量,应用窗口图形布局以及应用窗口图形绘制、纹理和多边形生产,发送纹理和多边形到GPU;而GPU主要是将CPU生产的纹理和多边形进行栅格化以及合成操作。

Android4.1之前,在绘制的时候没有加入垂直同步,只是在前缓冲区和后缓冲区切换的时候加入的情况下,如下图

18582563-54586db0b81652ae

我们知道,有生产有消费,一般是希望两者保持一个平衡状态,一方不能太快,而另一方太慢的情况。若像上图没在上层既生产端也加入垂直同步的话,那么可能存在如下图的问题:

18582563-2917aae576dfe5be

从上图我看出,在第0帧的时候,上层CPU开始产生第一帧的纹理,计算完成交给GPU进行栅格化合成,于是在第1个VSync信号到来时,屏幕正确显示第1帧,在下一个VSync到来之前,CPU开始处理第2帧的纹理,但是在第2个VSync到来的时候,GPU还没栅格化合成第2帧,因此,屏幕智能继续展示第1帧,这样就造成卡顿的想象发生。

因为上层不知道垂直同步信号什么时候发出,导致CPU本来可以提前在合适的实际进行绘制的也没进行,导致后面GPU合成没有在下一个垂直同步到来之前合成完成。因此,谷歌在4.1系统上绘制也加上了垂直同步,既生产和消费保持一个同步状态,如下所示:

18582563-cc1f1dcdaccc2d36

那么这个是如何进行垂直同步的呢,这个时候,我们就要搬出我们在很多很多中间件(如性能上报这种中间件)需要用到的一个类——Choreographer。该类实现了我们需要的VSync垂直同步,主要是向SurfaceFlinger注册一个VSync信号接收器DisplayEventReceiver,同时在Choreographer的CallbackQueue中注册有需要同步信号的组件(如ViewRoot, ViewRootImpl), 收到这个信号之后CallbackQueue对应的组件就开始执行刷新操作,进而开始绘制,完成绘制同步的操作。这样,加上垂直同步的相关帧表现情况如下图:

18582563-91b0f4384d9b5441

从第0帧开始,CPU开始处理第1帧的纹理,处理完成之后GPU进行合成,在第1帧到来时候,显示屏显示第1帧,接着同步的CPU可以开始处理第2帧的纹理,GPU在CPU处理完成之后栅格化合成,在第2个垂直同步信号到来的时候,显示屏可以展示第2帧。

然而上面的情况是理想情况,如果发生下图所示的情况,那么仍然会出现丢帧卡顿的情况。

18582563-1a1b2c62fed2a16d

如上图,若CPU绘制纹理完成而GPU没能在下一个VSync信号到来的时候完成合成,那么就存在在下一个16.6ms中展示新的一帧,此时仍然展示的前缓冲区的帧数据, 本来在下一个VSync到来之后,CPU也要开始进行绘制纹理的,因为没有多余的缓冲区可以用于操作,所以CPU只能空闲,在下下个VSync信号到来的时候,前面绘制的帧数据已经合成,可以被显示屏展示了,此时前置缓冲区就变成后置缓冲区,从而CPU就可以有缓冲区进行绘制处理了。

出现上面的问题是因为只有两个缓冲区,从而导致CPU还空闲了一段时间,假设在第一个VSync信号到来的时候有多余的缓冲区,那么CPU就可以同步进行,然后为GPU争取更多的时间去合成,从而避免影响后面接着卡顿的情况发生。因此就有了三缓冲区,如下图。

18582563-e3f95497f1026b71

SurfaceFlinger占据一个缓冲区,GPU合成的时候占据一个缓冲区,此时第一个VSync信号到来的时候,会发生一次卡顿,然而CPU可以利用第三个缓冲区进行纹理绘制,从而加速GPU可以第一时间进行合成操作。减少后面发生卡顿的情况的发生。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值