显示系统基础知识
在一个典型的显示系统中,一般包括CPU、GPU、Display三个部分,
CPU负责计算帧数据,把计算好的数据交给GPU,
GPU会对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)里存起来,
Display(屏幕或显示器)负责把buffer里的数据呈现到屏幕上。
双缓存
屏幕刷新频率是固定的,比如每16.6ms从buffer取数据显示完一帧,
理想情况下帧率和刷新频率保持一致,即每绘制完成一帧,显示器显示一帧。但是CPU/GPU写数据是不可控的,
所以会出现buffer里有些数据根本没显示出来就被重写了,即buffer里的数据可能是来自不同的帧的, 当屏幕刷新时,此时它并不知道buffer的状态,因此从buffer抓取的帧并不是完整的一帧画面,一个屏幕内的数据来自2个不同的帧,画面会出现撕裂感。简单说就是Display在显示的过程中,buffer内数据被CPU/GPU修改,导致画面撕裂。
怎么解决画面撕裂呢? 那就是使用双缓存。
由于图像绘制和屏幕读取使用的是同个buffer,所以屏幕刷新时可能读取到的是不完整的一帧画面。
双缓存,就是让绘制和显示器拥有各自的buffer:GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Frame Buffer,
当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行交换。如下图:
VSync
问题又来了:什么时候进行两个buffer的交换呢?
当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的扫描,此时有一段时间空隙,称为 VerticalBlanking Interval (VBI)。
这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现 screen tearing的状况。
VSync(垂直同步)是VerticalSynchronization的简写,它利用VBI时期出现的 vertical sync pulse
(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。
在Android4.1之前,屏幕刷新也遵循 上面介绍的 双缓存+VSync 机制。但还是会存在丢帧问题,具体如下:
1、Display显示第0帧数据,此时CPU和GPU会渲染第1帧画面,且在Display显示下一帧前完成了渲染
2、因为渲染及时,Display在第0帧显示完成后,也就是第1个VSync后,缓存进行交换,然后正常显示第1帧
3、接着第2帧开始渲染,但是是到了第2个VSync快来前才开始渲染的。
4、第2个VSync到来时,由于第2帧数据还没有准备就绪,缓存没有交换,显示的还是第1帧。这种情况被Android开发组命名为“Jank”,即发生了丢帧。
5、当第2帧数据准备完成后,它并不会马上被显示,而是要等待下一个VSync 进行缓存交换再显示。
所以总的来说,就是屏幕平白无故地多显示了一次第1帧。原因是 第2帧 的CPU/GPU计算 没能在VSync信号到来前完成 。
我们知道,
双缓存的交换是在Vsyn到来时才进行的,交换后屏幕会取 Frame buffer 内的新数据,而实际 此时的 Back buffer 就可以供GPU准备下一帧数据了。
如果 Vsyn 到来时,CPU/GPU就开始操作的话,是有完整的16.6ms的,这样应该会基本避免jank的出现了(除非CPU/GPU计算超过了16.6ms)。
那如何让 CPU/GPU 计算在Vsyn到来时进行呢?
为了优化显示性能,Google在Android 4.1系统中对Android Display系统进行了重构,实现了 Project Butter(黄油工程):
即系统在收到 VSync pulse
后,将马上开始下一帧的渲染,一旦收到 VSync ,CPU和GPU 才立刻开始计算然后把数据写入buffer。
CPU/GPU 根据VSYNC
信号同步处理数据,可以让CPU/GPU有完整的16ms时间来处理数据,减少了jank。
问题又来了,如果界面比较复杂,CPU/GPU的处理时间较长 超过了16.6ms呢?这时候就又会出现jank了。
某个时间段内,因 GPU 还在处理 B 帧,缓存没能交换,导致 A 帧被重复显示。而B完成后,又因为缺乏VSync pulse信号,它只能等待下一个signal的来临。当下一个VSync出现时,CPU/GPU马上执行操作(A帧),且缓存交换,相应的显示屏对应的就是B。这时看起来就是正常的。只不过由于执行时间仍然超过16ms,导致下一次应该执行的缓冲区交换又被推迟了——如此循环反复,便出现了越来越多的“Jank”。
分析以上原因,很容易是以下根本原因导致的:
只有两个 buffer,Back buffer 正在被GPU用来处理B帧的数据, Frame buffer 的内容用于Display的显示,这样两个buffer都被占用,CPU 则无法准备下一帧的数据。
那么,如果再提供一个buffer,CPU、GPU 和显示设备都能使用各自的buffer工作,互不影响,是不是就解决问题了呢?
三缓存
三缓存就是在双缓冲机制上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度的利用空闲时间,带来的坏处是多使用的一个 Graphic Buffer 所占用的内存。
如果已经发生了一个jank(丢失了B帧),但是在第二个 16ms 时间段,CPU/GPU 使用 第三个 Buffer 完成C帧的计算,虽然还是会多显示一次 A 帧,但后续显示就比较顺畅了,有效避免 Jank 的进一步加剧,如图所示:
以上就是Android屏幕刷新的原理了。
Choreographer
上面讲到,Google在Android 4.1系统中对Android Display系统进行了优化:在收到 VSync pulse 后,将马上开始下一帧的渲染。即一旦收到VSync通知,CPU和GPU就立刻开始计算然后把数据写入buffer。本节就来讲 “drawing with VSync” 的实现——Choreographer。
Choreographer,是一个Java类,包路径android.view.Choreographer。类注释是“协调动画、输入和绘图的计时”。
它使得Android系统收到VSync信号才开始绘制,保证绘制拥有完整的16.6ms,避免绘制的随机性。
通常应用层不会直接使用Choreographer,而是使用更高级的API,例如动画和View绘制相关的ValueAnimator.start()、View.invalidate()等。
业界一般通过Choreographer来监控应用的帧率。
在Activity启动过程中,会执行到onResume生命周期,这个过程会执行到ActivityThread
的handleResumeActivity
方法,
该方法中,会将跟布局DecorView添加到Window上,
同时,这个过程会创建一个ViewRootImpl对象,并调用ViewRootImpl对象的setView
方法,将DecorView赋值给ViewRootImpl的mView这个成员变量,然后执行requestLayout()
方法,
requestLayout()
方法又会执行到scheduleTraversals()
方法,接着就会走到performTraversals()
方法,接着到了我们熟知的测量、布局、绘制三大流程了。
另外,查看源码发现,当我们使用 ValueAnimator.start()、View.invalidate()时,最后也是走到ViewRootImpl的scheduleTraversals()方法。即 所有UI的变化都是走到ViewRootImpl的scheduleTraversals()方法。
那么问题又来了,scheduleTraversals() 到 performTraversals() 中间经历了什么呢?是立刻执行吗?
答案很显然是否定的,根据我们上面的介绍,在VSync信号到来时才会执行绘制,即performTraversals()方法。
1、首先使用mTraversalScheduled字段保证同时间多次更改只会刷新一次,例如TextView连续两次setText(),也只会走一次绘制流程。
2、然后把当前线程的消息队列Queue添加了同步屏障,这样就屏蔽了正常的同步消息,保证VSync到来后立即执行绘制,而不是要等前面的同步消息。
调用了mChoreographer.postCallback()方法,发送一个会在下一帧执行的回调,即在下一个VSync到来时会执行TraversalRunnable–>doTraversal()—>performTraversals()–>绘制流程。