[关闭]
@TryLoveCatch 2022-04-27T09:20:27.000000Z 字数 12693 阅读 2902

Android知识体系之屏幕显示

Android知识体系


参考

https://mp.weixin.qq.com/s/aavPtzbx57cHmpPX6S2SMg
https://mp.weixin.qq.com/s/A7RfxTsPXmbHv0oA6K9Ecg
https://mp.weixin.qq.com/s/cg8cpnejCBXNBxKBC7P5iQ
https://mp.weixin.qq.com/s/BwLMZDB2kaqdu_40ImZH7g
https://mp.weixin.qq.com/s/Zw9m7cO8YgirlAHe_f5NjQ
https://mp.weixin.qq.com/s/cxcQnwAHxBgBZMm0qUe1EA
https://www.cnblogs.com/dasusu/p/8311324.html
https://cloud.tencent.com/developer/article/1602130
https://github.com/huanzhiyazi/articles/issues/28
https://blog.csdn.net/qq_29882585/article/details/108419556

基础知识

显示系统

在一个典型的显示系统中,一般包括CPU、GPU、display三个部分, CPU负责计算数据,把计算好数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到buffer里存起来,然后display(有的文章也叫屏幕或者显示器)负责把buffer里的数据呈现到屏幕上。

显示过程,简单的说就是CPU/GPU准备好数据,存入buffer,display每隔一段时间去buffer里取数据,然后显示出来。display读取的频率是固定的,比如每个16ms读一次,但是CPU/GPU写数据是完全无规律的。

上述内容概括一下,大体意思就是说,屏幕的刷新包括三个步骤:

什么是帧缓存(FrameBuffer)

我们知道,在计算机中表示一张图像实际就是在一张足够密集的网格中,给每一个网格单元设置不同的像素值(比如用 RGB 表示的像素值)。而手机屏幕就是这样一张网格,每个网格单元都可以”涂上“不同的像素值,所以在屏幕上绘制图像就是把该图像的像素值涂到屏幕的网格里。
那么在将图像涂到屏幕上之前,必须先有一个地方提前把图像的像素值存储起来,也就是说要有一块内存区,里面存储了一整屏画面的像素值,若要显示该屏画面,就从这块内存里把像素值一个个取出来”填“到屏幕对应的格子里。这块内存区叫做 FrameBuffer,即 帧缓冲区,实际上它是一块虚拟空间,其对应的物理空间可能在物理内存也可能在显存。

假设屏幕的分辨率是 1920×1080,即屏幕网格有 1920×1080 个像素格子,那么 framebuffer 就是一个长度为 1920×1080=2073600 的一维数组,数组中的每个元素对应一个屏幕格子中的像素值。

在 Linux 中,framebuffer 既是一块缓冲区,也是一个设备,其设备文件名为 /dev/fb[0|1|...|31],主设备号为29,每个设备文件表示一个显示设备,因为允许多个屏幕。所以,理论上,可以通过 open、mmap、ioctl 系统调用来读写 FrameBuffer,从而实现图像绘制。关于 Linux设备,可以参考 Android匿名共享内存(ashmem)原理 中的有关说明。

Android 是基于 Linux 的,所以在 Android 设备上绘制图像同样也是通过读写 FrameBuffer 来实现的。所以我们 可以把表示 Android 主屏幕的 FrameBuffer 作为一个全屏窗口来看待。

需要注意的是,要将 FrameBuffer 中的数据真正显示到屏幕上,还必须通过总线将数据拷贝到显示屏存储空间。所以在分析绘制原理的时候,我们可以把 FrameBuffer 直接表示为显示设备,而在分析显示过程的时候应该还有一个 FrameBuffer -> display 的过程。

刷新频率

屏幕的刷新频率(Refresh Rate),就是一秒内屏幕刷新的次数。

我们知道,在某一个时刻,将图像数据涂到屏幕上我们就能直观地看到一幅静态的画面,但这显然不能满足用户需求。我们需要看到的是屏幕上的动画——即不断切换的连续衔接的画面。在动画术语中,每一张这样的衔接画面被称作帧。也就是说,为了看到动画,我们需要以恒定的速度取到连续的帧,并将帧涂到屏幕上。

如上,要显示屏幕动画,我们要设置两个时机:

对于设备而言,其屏幕的刷新频率 就相当于显示帧的时机和速度,可以看做是额定不变的(而生成帧速度对应我们通常说的帧率)。

刷新频率这个参数是手机出厂就决定的,取决于硬件的固定参数。目前大多数设备的刷新率是 60Hz,也就是一秒刷新60次,所以每次屏幕刷新的过程占用时间就是16ms(1000/60)左右,这个是固定参数,运行过程中,不会发生改变。

高刷手机

高刷手机,一般就是指高刷新率屏幕,也就是大于一般的60hz,比如90hz,120hz等等。

它的特点就在于每秒刷新的频率更高,使得画面更加流畅,顺滑,就算出现丢帧等情况,画面还能保证一个稳定性。

帧率

帧率(Frame Rate)就是GPU一秒内绘制操作的帧数,单位是fps,这个值并不是一个固定值,会根据设备运行情况有所变动

小结

屏幕成像原理

首先从过去的 CRT 显示器原理说起。

CRT 的电子枪按照上面方式,从左到右,从上到下,一行行扫描(逐行扫描或者隔行扫描),扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。

HSync

水平同步脉冲(Horizontal synchronization pulse, Hsync)是一个短小的脉冲,在一行扫描完成之后,它就会出现,指示着这一行扫描完成,同时它也指示着下一行将要开始。
Hsync加在两个扫描行之间,如上图中蓝色虚线

水平同步脉冲出现后,会有一小段叫horizontal back porch的时间,这段时间里的像素信号是不会被显示出来,过了这一小段时间之后,电子枪就开始扫描新的一行,将要显示的内容扫描到显示器上。

VSync

垂直同步脉冲(Vertical synchronization, Vsync)跟水平同步脉冲类似,但它指示着前一帧的结束,和新一帧的开始。
进入下一轮扫描之前有一个空隙,这段空隙时间叫做 VBI(Vertical Blanking Interval)如上图中黄色虚线

垂直同步脉冲是一个持续时间比较长的脉冲,可能持续一行或几行的扫描时间,但在这段时间内,没有像素信号出现。这个时间也比较重要,后面会用于缓存buffer数据交互。

对于刷新频率是60Hz的设备来说,VSync也是16ms一次

CPU & GPU

CPU 通常存在的问题的原因是存在非必需的视图组件,它不仅仅会带来重复的计算操作,而且还会占用额外的 GPU 资源。


Resterization 栅格化是绘制那些 Button、Shape、Path、String、Bitmap 等组件最基础的操作。它把那些组件拆分到不同的像素上进行显示。
这是一个很费时的操作,GPU 的引入就是为了加快栅格化的操作。

CPU 负责把 UI 组件计算成 Polygons,Texture 纹理,然后交给 GPU 进行栅格化渲染。

数据从哪儿来?

一般来说,计算机系统的CPU、GPU、显示器是以一种类似于串行的方式协同工作的。

如下图所示:

什么时候进行两个buffer的交换呢?

问题又来了:什么时候进行两个buffer的交换呢?

假如是 Back buffer准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的。看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。

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

VSync(垂直同步)是VerticalSynchronization的简写,它利用VBI时期出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。

所以说V-sync这个概念并不是Google首创的,它在早年的PC机领域就已经出现了。

Bad Case

什么是撕裂

一个屏幕内的数据来自2个不同的帧,画面会出现撕裂感

单缓冲

帧缓冲区只有一个,GPU向帧缓冲区提交渲染好的数据,视频控制器从帧缓冲区读取数据显示到屏幕上(典型的生产者—消费者模型)。
这时帧缓冲区的读取和刷新都都会有比较大的效率问题。

什么是画面撕裂呢?我们先从单缓冲说起:

屏幕刷新频是固定的,比如每16.6ms从FrameBuffer取数据显示完一帧,理想情况下帧率和刷新频率保持一致,即每绘制完成一帧,显示器显示一帧。
但是CPU/GPU写数据是不可控的,所以会出现FrameBuffer里有些数据根本没显示出来就被重写了,即FrameBuffer里的数据可能是来自不同的帧的, 当屏幕刷新时,此时它并不知道FrameBuffer的状态,因此从FrameBuffer抓取的帧并不是完整的一帧画面,即出现画面撕裂。

简单说就是:

Display在显示的过程中,FrameBuffer内数据被CPU/GPU修改,导致画面撕裂。
要注意,渲染也是一个过程,并不是一蹴而就的,记住这个

比如:

绘制到第N帧的A行时,GPU把N+1帧数据进行渲染,更新了FrameBuffer的数据,但是屏幕会继续接着从A+1行绘制,这样导致前A行是N帧的数据,后面几行是N+1帧的数据,在我们看来就是两张图片撕开各取一部分拼起来,就导致了撕裂。

所以根本原因是:
显示帧的时候,FrameBuffer被更新了,也就是说GPU渲染了,所以解决方法应该是如何来控制GPU渲染的时机。

双缓存?


撕裂发生的原因是display读buffer同时,buffer被修改,那么多一个buffer是不是能解决问题,这个时候双缓冲就诞生了。
双缓存,让绘制和显示器拥有各自的buffer:GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Frame Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行交换。

我们可以看出来,这个涉及到交互数据的时机,什么时候进行两个buffer的交换呢?目前有两个时机:

假如是 Back buffer准备完成一帧数据以后就进行交换,那么如果此时屏幕还没有完整显示上一帧内容的话,这时,就会再次出现撕裂了。
渲染也是一个过程,并不是一蹴而就的
比如说,屏幕显示Frame Buffer N帧的A行,这个时候,Back buffer准备好了,立马交换/赋值,那么从A+1行开始,就是绘制的N+1帧的数据了,所以还是会撕裂的

不能在Back buffer准备就绪之后,就立即交换数据,需要在屏幕处理完一帧数据后,才可以执行这一操作,也就是VSync

双缓存单独是并不能解决撕裂!!

VSync

解决方法:

VSync垂直同步,来控制CPU/GPU的渲染到帧缓存的时机

VSync做了几件事:

所以每一个VSync间隔的时间 = VBI时间 + 屏幕显示一帧的时间

增加VSync之后,CPU/GPU在渲染每一帧之前会等待VSync信号,只有显示器完成了一次刷新时,发出VSync信号,CPU/GPU才会渲染下一帧。
这种情况下,CPU/GPU的渲染能力会受到显示器刷新率的制约。如果显示器刷新率是60Hz,CPU/GPU帧率最多只会达到60。

VSync+单缓冲可以吗?

撕裂的根本原因是:显示帧的时候,FrameBuffer被更新了,也就是说GPU渲染了,所以我们使用了VSync,来控制GPU渲染的时机。

那么问题来了,单单只用VSync能不能解决撕裂问题呢?

答案是可以的,但是效率很低。

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

所以: VSYNC真正「解决了撕裂问题」,而双缓「冲优化了卡顿问题」。

小结

VSYNC真正「解决了撕裂问题」但是效率低,而双缓「冲优化了卡顿问题」

什么是卡顿

帧率 < 刷新频率的时候,就会出现卡顿,而刷新频率是不会改变的,所以需要提高帧率

没有VSync

这里说的没有Vsync,不是说没有Vsync,Vsync是一直有的,这里指的是,CPU/GPU生成下一帧不依赖于Vsync触发

如图所示:

这样,第1帧多显示了一次,这个叫 jank现象,在用户看来是画面产生了卡顿。

有VSync

如果我们加上 Vsync 机制,让第2帧的生成过程提速到第一个 Vsync 信号产生之时,则我们会得到一个流畅的画面,如下图所示:

VSync同步使得CPU/GPU充分利用了16.6ms时间,减少jank

超过了16.6ms?

上面也是说了,VSync充分利用16.6ms,双缓存只能优化卡顿,并不能根本解决卡顿。

如果界面比较复杂,CPU/GPU的处理时间较长 超过了16.6ms呢?

当帧率超过 16ms,采用双缓冲机制仍然可能产生 jank现象。更要命的是,产生 Jank 的那一帧的显示期间,GPU/CPU 都是在闲置的。

三缓存

接下来说一下三缓存的优缺点:

所以,是不是 Buffer 越多越好呢?这个是否定的,Buffer 正常还是两个,系统并非一直开启三缓冲。

四缓存?

我们可以考虑一下帧率超过 32ms 的情况,在这种情况下:

图中我们画出了两条红线,代表没有被利用到的 Vsync 信号。

为了解决这个问题,我们可以依样画葫芦再增加一个缓冲区,由三缓冲变成四缓冲:

如上图,我们增加了一个缓冲区D,在第二个 Vsync 信号到达时弥补了三缓冲情况下的生成帧时机缺口。

由于帧率超过 32ms 而设置四缓冲,导致的结果就是往后的每一帧都比实际预计的显示时间延迟了 32ms。例如,当前计算A的数据时,说明正在显示D,之后需要B和C都显示了,才能再次显示A,所以相当于时晚了32ms,也就是B和C的时间。
从理论上也不难分析,因为屏幕的刷新频率快于帧率,为了让所有的帧都有机会显示,只能把来不及显示的帧缓存起来,等到延后一段合适的时间后再显示,不断重复这个过程,这类似于流水线。

从用户的视角看,就相当于屏幕的所有交互动画显示都会比操作的时间慢 32ms,这对于人眼来说勉强还能接受(人眼能接受的帧率是 24fps,即每帧 41ms)。
不过为了给体验留足空间,我们的帧生成时间应该追求控制在 16ms,对于游戏来说要控制在 32ms 以内。
所以四缓冲在理论上是可行的,而实际中大多不太采用,更不用说更多的缓冲机制了,主要时考虑操作延迟

小结

解决

https://mp.weixin.qq.com/s/Zw9m7cO8YgirlAHe_f5NjQ
https://mp.weixin.qq.com/s/BwLMZDB2kaqdu_40ImZH7g
https://mp.weixin.qq.com/s/cxcQnwAHxBgBZMm0qUe1EA

PIPELINE 管道
PROBLEM 发生的问题
TOOLS 用什么工具来解决
SOLUTION 解决方案时什么

在CPU方面,最常见的性能问题是不必要的布局和失效,这些内容必须在视图层次结构中进行测量、清除并重新创建,引发这种问题通常有两个原因:

在GPU方面,最常见的问题是我们所说的过度绘制(overdraw),通常是在像素着色过程中,通过其他工具进行后期着色时浪费了GPU处理时间。

下面我们对GPU和CPU产生的两大问题进行优化:

Overdraw(过度绘制)

屏幕上的某个像素在同一帧的时间内被绘制了多次。

在多层次的UI结构里面, 如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源。

按照以下步骤打开Show GPU Overrdraw的选项:
设置 -> 开发者选项 -> 调试GPU过度绘制 -> 显示GPU过度绘制

蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况:

我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。

去掉window的默认背景

当我们使用了Android自带的一些主题时,window会被默认添加一个纯色的背景,这个背景是被DecorView持有的。

当我们的自定义布局时又添加了一张背景图或者设置背景色,那么DecorView的background此时对我们来说是无用的,但是它会产生一次Overdraw,带来绘制性能损耗。

去掉window的背景可以在onCreate()中setContentView()之后调用getWindow().setBackgroundDrawable(null);或者在theme中添加android:windowbackground="null"。

去掉其他不必要的背景

有时候为了方便会先给Layout设置一个整体的背景,再给子View设置背景,这里也会造成重叠,如果子View宽度mach_parent,可以看到完全覆盖了Layout的一部分,这里就可以通过分别设置背景来减少重绘。

再比如如果采用的是selector的背景,将normal状态的color设置为“@android:color/transparent”,也同样可以解决问题。

所以开发过程中我们为某个View或者ViewGroup设置背景的时候,先思考下是否真的有必要,或者思考下这个背景能不能分段设置在子View上,而不是图方便直接设置在根View上。

clipRect的使用

我们可以通过canvas.clipRect()来 帮助系统识别那些可见的区域。

这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠 组件的自定义View来控制显示的区域。

同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。

ViewStub

ViewStub称之为“延迟化加载”,在较多数情况下,程序无需显示ViewStub所指向的布局文件。

只有在特定的某些较少条件下,此时ViewStub所指向的布局文件才需要被inflate。且此布局文件直接将当前ViewStub替换掉,具体是通过 viewStub.infalte()或 viewStub.setVisibility(View.VISIBLE) 来完成。

Merge标签

Merge标签可以干掉一个view层级。

Merge的作用很明显,但是也有一些使用条件的限制。有两种情况下我们可以使用Merge标签来做容器控件。

第一种子视图不需要指定任何针对父视图的布局属性,就是说父容器仅仅是个容器,子视图只需要直接添加到父视图上用于显示就行。

另外一种是假如需要在 LinearLayout 里面嵌入一个布局(或者视图),而恰恰这个布局(或者视图)的根节点也是 LinearLayout,这样就多了一层没有用的嵌套,无疑这样只会拖慢程序速度。而这个时候如果我们使用merge根标签就可以避免那样的问题。

另外Merge只能作为XML布局的根标签使用,当Inflate以开头的布局文件时,必须指定一个父ViewGroup,并且必须设定attachToRoot为true。

其他

扩展

离屏渲染

flutter的原理:
PlatformView 可以嵌套原生 View 到 Flutter UI 中,这里面其实是使用了 Presentation + VirtualDisplay + Surface 等实现的,大致原理就是:

使用了类似副屏显示的技术,VirtualDisplay 类代表一个虚拟显示器,调用 DisplayManager 的 createVirtualDisplay() 方法,将虚拟显示器的内容渲染在一个 Surface 控件上,然后将 Surface 的 id 通知给 Dart,让 engine 绘制时,在内存中找到对应的 Surface 画面内存数据,然后绘制出来。em... 实时控件截图渲染显示技术。

丢帧真的是丢弃了帧吗

延迟显示,因为缓存交换的时机只能等下一个VSync了。

布局层级较多/主线程耗时 是如何造成 丢帧的呢?

布局层级较多/主线程耗时 会影响CPU/GPU的执行时间,大于16.6ms时只能等下一个VSync了。

16.6ms刷新一次 是啥意思?是每16.6ms都走一次 measure/layout/draw ?

屏幕的固定刷新频率是60Hz,即16.6ms。不是每16.6ms都走一次 measure/layout/draw,而是有绘制任务才会走,并且绘制时间间隔是取决于布局复杂度及主线程耗时。

measure/layout/draw 走完,界面就立刻刷新了吗?

不是。measure/layout/draw 走完后 会在VSync到来时进行缓存交换和刷新。

可能你知道VSYNC,这个具体指啥?在屏幕刷新中如何工作的?

当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时会出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。并且Android4.1后 CPU/GPU的绘制是在VSYNC到来时开始。

可能你还听过屏幕刷新使用 双缓存、三缓存,这又是啥意思呢?

双缓存是Back buffer、Frame buffer,用于解决画面撕裂。三缓存增加一个Back buffer,用于减少Jank。

可能你还听过神秘的Choreographer,这又是干啥的?

用于实现——"CPU/GPU的绘制是在VSYNC到来时开始"。
参考:https://mp.weixin.qq.com/s/cg8cpnejCBXNBxKBC7P5iQ

代码中修改了UI,屏幕是怎么进行刷新的?

当我们用代码修改了UI,比如使用了setText,修改Textview的值。这时候屏幕不会马上绘制刷新。而是会调用到invalidate方法请求重绘,然后会向VSYNC服务发送请求,等到下一个VSYNC信号触发的时候,就开始上面说过的流程,也就是处理数据,绘制图像,具体所做的工作就是测量—布局—绘制。接着,屏幕在下下一个VSYNC信号的时候,拿到缓存区中绘制好的图像并显示到屏幕上了。

所以任何UI的改变,都要遵从上述所说的VSYNC机制,只是这个过程很短。当然为了保证最快时间绘制到屏幕上,而不让其他消息影响到VSYNC的响应速度,就加入了同步屏障。

如果界面保持静止不变,屏幕会刷新吗?

首先,屏幕刷新频率这个是不会变的,也就是每隔16ms左右就会进行一次刷新,而刷新的帧数据就是我们的程序内部在接收到刷新的vsync信号之后,经过计算绘制后的图像数据。

但是,app并不是每一个vsync信号都能接收到的,只有当应用有绘制需求的时候,才会通过scheduledVsync 方法申请VSYNC信号,然后下一个屏幕刷新的信号才能被我们的程序所接收到,也就是Choreographer类的onVsync方法才能被执行,然后就开始测量—布局—绘制等工作了。

所以,如果界面不变化,我们的程序就收不到VSYNC信号,也就无法处理数据进行绘制了。只有当需要改变界面的时候,才会去申请这个屏幕刷新服务,才能接收到VSYNC信号。这种情况下,屏幕还会进行刷新,只不过刷新的都是同样的图像数据。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注