学不会Android显示系统?那是因为你还没有看过这篇文章

发布于:2024-05-08 ⋅ 阅读:(16) ⋅ 点赞:(0)

前言

本文目标:一篇文章,介绍Android显示系统的全貌与联结细节。

Android显示系统,是Android知识体系中一个非常重要的组成部分。酝酿许久,写出此文,希望能最大化降低各位的学习成本,给各位应有的帮助。

一. 整体介绍

Android显示系统的内容,太大,太深了。内容之繁杂,让我很难只通过一篇文章,就介绍详尽。而我已经给自己立下了目标了,不能轻易退缩。因此,冥思苦想许久,决定本文将详细介绍以下几个角色的作用,以及相互配合的方式:

  • Surface
  • SurfaceFlinger
  • bufferQueue
  • HAL
  • VSync
  • frameBuffer
  • 多缓冲机制

我将尽可能将细节描述清楚,但篇幅有限,我也会在必要之处做些简化。如若有疏漏或错误,还请不吝赐教。

下面,用一幅图,概括这几个角色之间的合作关系

image.png

这个图,对于你来说,或许太过庞大,也根本无法理解。但没有关系,这里的图只是希望你有一个大概的了解。下面,我们对这些角色依次进行介绍

二. 分别介绍

Surface是什么?

1. 它的来源是这样的:

一个View对应一个Window,一个Window对应一个Surface,一个Surface对应一个Layer

2. 介绍

关于它的定义,我们还是从官方文档上看起。在的介绍中,说到,

翻译过来就是:持有一个,未经处理的,且未来将会被Compositor处理的buffer。其他的描述我们可以就不用关心了。但这句话,未免有点太简洁了点,对于初学的人极不友好。这里:buffer是什么?Compositor是什么?

buffer,你可以理解为,就是一块内存空间,这个空间,保存着图像的信息,比如图像的宽高,如何绘制等。Compositor你甚至可以直接理解为就是SurfaceFlinger。

也就是说,这个buffer,最后要被SurfaceFlinger处理,而这个buffer,是可以被Surface持有的。

一般来说,当手机显示一个android应用时,它至少有3个buffer,也可以说是3个Surface

顶部状态栏是一个Surface,底部系统导航栏也是一个Surface,App渲染的主界面,也是一个Surface。

3. Surface作用

Surface的作用,可以概括为以下4点

  • dequeue:向bufferQueue中申请Buffer,并将绘制好的图像数据写入buffer
  • queue:将带有图像数据的buffer插入bufferQueue
  • 管理一个窗口的各种属性,比如宽高,颜色格式等
  • 提供画布工具来绘制图像。比如SurfaceHolder可以返回canvas

一般来说,Surface对应的bufferQueue中,至少有2个buffer,以完成双缓冲机制渲染,即一个用于绘制,一个用于显示。

4. 题外话:Surface和Window的区别是什么?

细心的朋友很快就发现了,Surface和Window怎么这么像?确实如此。

我对这两者的理解是这样的:Window是Android应用层面关心的角色,在我们进行应用开发时,只关心Window。而到了Android的更底层,比如要把图像渲染出来,那Android系统关心的就是Surface了。也就是说,Window是开发层面的角色,Surface是渲染实现层面的角色。Surface是从Window演变的,有自己实现者角色独有的一些特点,比如具有bufferQueue,操作bufferQueue的能力等等,这些Window都是没有的

SurfaceFlinger是什么?

介绍中,说到

翻译过来,就是:SurfaceFlinger接受、合成buffer,并且将buffer传递给设备。而且它和WindowManager还有着密切的合作,即WindowManager提供给SurfaceFlinger,待合成的buffer,以及window的一些数据,这些数据可以被SurfaceFlinger用来将Surface合成,并将合成后的产物,传递给设备

让我们再详细介绍下。

在bufferQueue看来,Surface是生产者,那么SurfaceFlinger就是消费者。

SurfaceFlinger是一个系统服务,接受WindowManager的Surface作为输入,根据大小,位置,z轴顺序等参数,计算出每个Surface最终的位置,然后交给HWC或者OpenGL生成最终的Buffer,然后交给设备进行画面显示。

SurfaceFlinger提供至少2种Surface合成服务:

  • Client合成,又叫软件合成,GPU合成,GL合成。这个合成过程,由OpenGL完成。对于不能用硬件方式合成的Surface,都需要委托OpenGL进行合成,比如背景模糊,或者Surface数超过硬件合成的限制等
  • Device合成,又叫硬件合成。专门用于硬件合成的模块叫HWC(Hardware Composer),HWC是一个典型的HAL(Hardware Abstraction Layer) 模块

且一般来说,Device合成的性能会比Client合成的性能要好。两种合成协作的方式如下

image.png

题外话,SurfaceFlinger 启动后,其他几个重要的初始化

  • GL渲染引擎初始化,用于 Client合成。

  • Gralloc 客户端初始化,即建立 SurfaceFlinger 与 Gralloc HAL 之间的联系,用于缓冲区的分配。

  • HWC 客户端初始化,即建立 SurfaceFlinger(客户端) 与 HWC HAL(服务方) 之间的联系,用于 Device合成以及注册 VSync 通知触发合成过程。

  • 初始化并启动事件队列(类似于 Android 应用层的 Handler 机制),诸如 VSync 通知合成、显示屏修改插拔等都将以事件的形式通知 SurfaceFlinger 进行处理。

bufferQueue是什么?

介绍说

在前面关于Surface和SurfaceFlinger的介绍中,不止一次的说过bufferQueue。让我们详尽地来剖析一下bufferQueue。

首先,很显然,它用到了生产者消费者模式,它连接了图像buffer的生产者,以及图像buffer的消费者。

  • 消费者创建并拥有bufferQueue数据结构,并且可能与生产者位于不同的进程中。

  • 当生产者需要一个缓冲区时,它会通过调用dequeueBuffer()从bufferQueue请求一个空闲的缓冲区,并指定缓冲区的宽度、高度、像素格式和使用标志。生产者填充缓冲区后,通过调用queueBuffer()将缓冲区返回给队列。

  • 接下来,消费者使用acquireBuffer()获取缓冲区并使用其内容。当消费者完成使用后,它通过调用releaseBuffer()将缓冲区返回给队列。

这是官方文档给我们的信息,让我们试图再深入一下。

即对于bufferQueue的操作,我们需要关注4种。即生产者发出的:dequeueBuffer和queueBuffer,以及消费者发出的:acquireBuffer和releaseBuffer。与这四种操作对应的,buffer的状态,也有四种,即:DEQUEUED、QUEUE、ACQUIRED、FREE

HAL是什么?

HAL全称为Hardware Abstraction Layer,即“硬件抽象层”。简单来说,就是谷歌,给Android的硬件系统,统一了一套接口,厂商需要按照这个接口来,至于具体怎么实现,厂商自己定。很好理解,对吧。

在整个Android系统的架构中,HAL的位置如上图所示。

在本篇关注的,Android的显示系统中,比较常用的两个HAL中的模块是:Gralloc和HWC。

1. Gralloc是什么

Gralloc是负责bufferQueue中Buffer的分配,主要包括:

  • 管理Buffer需要用到的内存,管理当然就有申请和回收。
  • 实现共享内存机制。因为Buffer的数据量很多,很大,不可能在进程之间,传递来传递去的。

2. HWC是什么?

HWC全称是:Hardware Compositor,在上文中我们也介绍过,它有如下功能

  • 提供Surface合成决策,即决定Surface是Client合成还是Device合成。
  • 执行Device合成。
  • 显示画面。

SurfaceFlinger 在执行合成之前,会询问 HWC 哪些Surface可以进行 Device合成,然后把不能进行 Device合成的Surface自行进行 Client合成作为新的Surface,然后统一与其它Surface一起交于 HWC 进行 Device合成。

3. 加餐:进程通信方式

值得注意的是:应用层和系统服务层之间用 binder 实现 IPC;系统服务和 HAL 之间用 hwbinder 实现 IPC;HAL 访问内核驱动则是通过系统调用来实现的。

frameBuffer是什么?

frameBuffer翻译过来就是“帧缓冲区”。什么是帧呢?在这张图中

这一时刻,这个画面,我们就叫它“一帧”。那么这一帧画面,其实是由非常多的像素点组成的。每一个像素点都保存了像素值,比如RGB色值。成千上万个像素点组合在一起,组成了这么一帧画面。很好理解,对吧?

那么在这一帧画面,真正显示之前,是不是得有一块区域,来存储这一帧画面的所有像素点的数据?这块区域,就是帧缓冲区,也就是frameBuffer。

比如说,屏幕的分辨率是1920 x 1080,那么frameBuffer就是包含了1920 x 1080个像素点元素的数组。数组中的每一个元素,就是屏幕上每一个像素点的像素值。

VSync是什么?

这么说:

但我认为,这些话,说了跟没说一样。受影响很大,下面我来详细介绍下什么是VSync。

1. 屏幕刷新率和帧率

首先要介绍下什么是屏幕刷新率。

我们都知道,手机之所以显示出动画效果,是很多帧画面,非常快速的连续显示形成的。60hz和120hz(即所谓的低刷和高刷手机)就是表示,每一秒,能刷新多少次,60hz就是一秒钟能刷新60次,也就是16ms刷新一次。之所以高刷的手机看起来更丝滑,更流畅,就是因为高刷的手机,每一秒钟,刷新的次数更多,帧和帧之间的变化更细微,更连贯,因此看起来更丝滑。这么一秒钟刷新多少次,就是屏幕刷新率,比如60hz和120hz就是屏幕刷新率

那么我们再细化一下这个过程。对于帧来说,其实有2个流程,是我们需要关注的。

  • 生成帧,产生了新的画面(帧),将其填充到 frameBuffer 中,这个过程由 CPU(计算绘制需求)和 GPU(完成数据绘制)完成;
  • 显示帧,显示屏显示完一帧图像之后,等待一个固定的间隔后,从 frameBuffer 中取下一帧图像并显示,这个过程由 GPU 完成。

我们刚刚所说的屏幕刷新率,其实是针对的“显示帧”这个概念。

那么帧率是什么呢?其实是“生成帧”这个概念。即生成帧的速度。也就是我们平常关注的fps(frame per second)。比如100fps,就是1秒钟,生成100帧画面。

这里,屏幕刷新率和帧率,其实是不一样的。但两者需要完成某正协作,才能使得动画完整连贯地显示出来。

在理想状态下,生成帧时机与显示帧时机保持完全一致,这同时也意味着屏幕刷新率和帧率相等,这样我们就能看到 可靠的动画,即无误且完整的动画序列。

2. VSync

刚刚说的是在理想情况下,我们生成帧和显示帧速度完全一致,那么我们就能看到非常完整丝滑的动画。

但现实当然不是这样。对于一个手机来说,显示帧的频率,即屏幕刷新率,在一段时间内,是基本固定的,要么60hz,要么120hz或者其他的固定数。但生成帧的频率即帧率,就不一定了。生成帧影响的因素太多了。比如手机的CPU、GPU算力,帧画面的复杂度,计算很耗时等等都会影响。

因此很有可能出现,生成帧的速度,和显示帧的速度不一致的情况

这会造成什么现象呢?画面撕裂,即tearing。

我用一个图来解释下这个过程

image.png

那么如何解决这个问题呢?VSync登场了。

在解释 VSync 之前,我们需要理解显示器的扫描顺序:对于一帧画面,在屏幕上的显示是按照先从左往右扫描完一行,然后从上往下扫描下一行的顺序来渲染的。当扫描完一屏之后,需要重新回到第一行继续刚才的过程,而在进入下一轮扫描之前有一个空隙,这段空隙时间叫做 VBI(Vertical Blanking Interval)。

在 VBI 期间,正好就是用于生成帧的最佳时间。而要保证这一点,我们需要在一屏扫描完进入下一轮扫描之前,即在一个 VBI 的开始时刻通知 CPU/GPU 去立即产生下一帧。恰好,硬件会在这个时刻触发垂直同步脉冲(Vertical Sync Pulse),正好可以用来进行通知,这个机制就叫 VSync。

在 Android 中,VSync 需要做的事是:产生 VSync 信号,通知 CPU/GPU 立即生成下一帧,通知 GPU 从 frameBuffer 中将当前帧 post 到显示屏。(后半句可以先不管)即,生成帧的时机必须和 VSync 信号保持同步。

在这里我们也可以发现,生成帧的时机,是VBI的开始时刻,显示帧的时机,是VBI的结束时刻。但因为VBI时间很短,因此我们通常把VBI这一个时间段,缩短成一个VSync信号这一时刻,来看待。

但VSync依然解决不了画面撕裂的问题。如果生成帧的速度慢于显示帧的速度,那么到了显示帧的时刻,即VBI结束的时刻,依然有可能出现「新一帧的画面没渲染完」的情况。这时候就将画面信息拿去渲染,依然会导致tearing现象。那么怎么去杜绝这个现象呢?

答案:将frameBuffer空间进行扩展,使得其至少为2倍的屏幕空间。即我们常说的双缓冲或者三缓冲机制。

双缓冲机制

双缓冲或者三缓冲机制,保证了,只有一帧被完全生成出来之后,才能被渲染,否则就不渲染。也就是说,它真的解决了撕裂问题。如下图

这里总共有2个缓冲区:缓冲区A和缓冲区B。在第1个VSync信号到来时,缓冲区B已经完成了“生成帧”这一步,缓冲区A刚展示完,处于空闲状态。因此,可以在第1个VSync到来时,可以让缓冲区B的帧显示到屏幕上,同时让缓冲区A开始执行“生成帧”这一步。后面的流程以此类推。所以双缓冲+VSync保证了,不会出现tearing现象。

我们刚刚看了只有VSync的情况,以及VSync和双缓冲一起的情况。那么如果只有双缓冲,没有VSync呢?

可以看到,在没有VSync的情况下,本来应该在第1个VSync信号到来时,执行缓冲区2的“生成帧”的操作。但这一步骤被延迟到了第2个VSync到来前后。从而导致在第2个VSync信号到来时,本来可以显示缓冲区2新生成的帧,但因为缓冲区2这时候才刚开始生成帧,所以依然显示了缓冲区1的画面。从而导致出现了Jank现象,即卡顿。而且导致缓冲区1被额外占用了一个周期,即有资源上的浪费。

其实在这里,出现Jank是正常的。不正常的是,如果没有VSync机制,会大大提升出现Jank的频率

因此,一个可靠的动画,VSync和双缓冲缺一不可。VSync缓解了Jank问题,双缓冲解决了撕裂问题

三缓冲机制

前面我们的假设,都是基于,帧率低于屏幕刷新率的情况。那么,对于帧率即“生成帧”的时间,超过屏幕刷新率的情况,会出现什么问题呢?

比如一个60hz的手机,一个帧生成的时间,超过了16ms,这时会出现什么情况呢?

从上图可以看到,缓冲区B的“生成帧”时间,要多于16ms,导致在第一次VSync信号发生时,缓冲区B的数据还没准备好,从而导致设备依然显示了缓冲区A的画面。导致了一次卡顿。后面因为同样的原因,导致了第二次卡顿。即,Jank的次数会大大增加。那么如何解决这个问题呢?

答案就是三缓冲机制,即再引入一个缓冲区。

这样,只会在第一次VSync的时候,发生一次卡顿,后面都不会卡顿了。而且顺序也是按照“生成帧”的顺序来的。虽然从预期应该”显示帧“的时机,到实际”显示帧“的时机,相隔了16ms,也就是说所有的帧都向后延迟了16ms显示,但这个时间太短,人眼也是察觉不到的。

也就是说,三缓冲机制,可以缓解在双缓冲机制的基础上,帧率(”生成帧“的速度)慢于屏幕刷新率(刷新帧的速度)时,出现的卡顿现象。

更多的缓冲机制?

从刚刚的分析来看,貌似缓冲区“越多越好”?但事实果真如此吗?

我们以一个60hz刷新率的手机为例。首先,在双缓冲机制+VSync机制的基础上,可以实现帧率小于等于16ms时,动画的完整播放。在三缓冲机制+VSync机制的基础上,可以缓解帧率大于16ms时,动画的卡顿情况,尽可能实现丝滑播放。 但如果帧率过慢,以至于都超过了16 x 2 = 32ms,那么这时候三缓冲机制也无能为力了,画面就会非常卡。为了解决这个问题,我们貌似可以“照葫芦画瓢”,再来一个缓冲区,即四缓冲机制。

我直接说结论吧,这样的确可以很大程度上缓解卡顿问题,但会导致预期应该”显示帧“的时机,到实际”显示帧“的时机,相隔了16 x 2 = 32ms。也就是说,用户做了一个操作,此时应该有相应动画响应,但是因为每一帧生成太过耗时,同时为了减少卡顿,我们又引入了四缓冲机制,导致动画的响应,都在32ms以后了。这虽然对人眼来说能勉强接受,但这也有点太过勉强。因此,我们一般不这么玩”极限操作“。一般来说,对于60hz的设备,我们的“生成帧”速度还是尽量控制在16ms之内,或者32ms极限。因此,这也是卡顿优化的价值所在。我们在执行动画时,不要太过耗时,否则卡顿就会非常严重。

人眼能分辨的帧率是24fps,即每帧生成的时间是41ms

因此,四缓冲机制在理论上来说是可行的,但实际上基本没这么用的。更别说更多的缓冲机制了。

3. 加餐:关于Jank和丢帧

卡顿和丢帧是一回事吗?

答案:不是

一个帧是否丢弃不用,关键在于该帧的绘制任务是否被最终执行了

每一个帧都对应一个帧绘制任务,每一个绘制任务都对应一个 VSync 信号,所谓的丢帧是原本该帧对应的绘制任务没有在其对应的 VSync 信号到来时被执行,但并不意味着其永远不再被执行了,只能说该帧没有得到及时地绘制。

其对应的绘制任务仍然缓存在任务队列中,延迟到将来的 VSync 信号到来时执行。

也就是说,卡顿是帧绘制任务延迟执行的结果,而丢帧是帧绘制任务被丢弃的结果。

4. 总结

VSync和多缓冲机制,解决了tearing问题,引入了jank问题。但jank问题用户可理解,且可以通过一定策略来缓解。

真正的多缓冲机制

绘制缓冲区和显示缓冲区

其实,在我们刚刚介绍的双缓冲、三缓冲机制中,缓冲区其实被分成了2种:

  • 绘制缓冲区
  • 显示缓冲区

比如

在双缓冲机制中,在第1个VSync到来之前,缓冲区B是“绘制缓冲区”,缓冲区A是”显示缓冲区“。看名字就知道他们的作用是什么。而frameBuffer是没有绘制缓冲区的。那么问题来了,绘制缓冲区是什么呢?

没错!就是bufferQueue。还记不记得,bufferQueue中,也有多个buffer,有时是2个buffer,可能还有更多。这是绘制缓冲区。显示缓冲区才是frameBuffer。而且有意思的是,缓冲区是绘制缓冲区,还是显示缓冲区,是会随着周期变化的。

image.png 只是前期为了叙述方便,都叫缓冲区A和缓冲区B了,实际上,他们是显示缓冲区和绘制缓冲区的角色转换。

对于绘制缓冲区,前面我们已经详细介绍过bufferQueue了,这里不再赘述。下面详细介绍下frameBuffer的显示缓冲区是怎么实现的。

显示缓冲区实现原理

frameBuffer的显示缓冲区,也是两个区域,分别是

  • 背景缓冲(backbuffer) :用于保存正在生成的帧。
  • 前景缓冲(frontbuffer) :用于保存即将显示的帧。

在显示机制上,有2种策略,即软件双缓冲和翻页双缓冲。

在软件双缓冲实现中,frameBuffer 本身全部作为 frontbuffer,其大小也只有一屏大小。VSync 到达后,有两个细分过程:先将数据从 backbuffer 拷贝到 frontbuffer,然后将数据从 frontbuffer 到显示。如图所示

image.png

翻页双缓冲中,frameBuffer的空间不再像软件双缓冲那样,只包括frontBuffer。

翻页双缓冲的frameBuffer,包括backBuffer和frontBuffer。在其实现中,只需要将 frameBuffer 一分为二:buffer0 和 buffer1。然后设置两个指针:frontBuffer指针和 backBuffer指针,它们分别指向 buffer0 或 buffer1,当显示完一帧之后,两个指针的指向进行互换即可,

image.png

在翻页双缓冲中,因为 frontbuffer 和 backbuffer 都在 frameBuffer 中,生成帧直接在 frameBuffer 中进行,所以无需通过总线拷贝数据,显示帧时机和生成帧时机也更可控,比软件双缓冲技术性能更好。

总结

说到这里,可能有点懵。没关系,我们再总结一下,我们所说的双缓冲机制,其实是这样的

image.png

我们所说的VSync中的双缓冲机制,是bufferQueue和frameBuffer结合的双缓冲。而bufferQueue和frameBuffer内部,又有自己的多缓冲策略。因此,这就组成了真正的多缓冲机制。

三. 总结

到这里,下面这个图你或许也就理解了

image.png

在这里,还有一些细节的实现原理,我都没有介绍,比如进程通信机制Binder,共享内存机制。如果读者感兴趣,后面会单独出相关的文章。

后记

在本文中,我通过介绍Surface、SurfaceFlinger、bufferQueue、HAL、frameBuffer、VSync、多缓冲机制,以及他们之间的配合方式,详细介绍了Android的显示系统。

在撰写本文的过程中,学习了很多博客,他们质量很高,推荐阅读