通俗易懂的Android屏幕刷新机制
前言
我们买手机的时候经常听说这个手机多少多少HZ刷新率。目前手机大多都是60HZ,现在有的手机都到144HZ的高刷新率了。这个刷新率指标是干什么的呢?屏幕又是如何将数据显示到Android手机屏幕上的呢?玩游戏时的卡顿是怎么形成的? 基于对这些问题的好奇,小研究了一番,就有了以下这些内容:
相关基础概念
人眼视觉残留
当物体在快速运动时, 当人眼所看到的影像消失后,人眼仍能继续保留其影像1/24秒左右的图像,这种现象被称为视觉暂留现象,是人眼具有的一种性质。
这是因为:人眼观看物体时,成像于视网膜上,并由视神经输入人脑,感觉到物体的像。但当物体移去时,视神经对物体的印象不会立即消失,而要延续1/24秒左右的时间。
逐行扫描
显示器并不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点。
帧、帧率(数)、刷新率
在视频领域,帧是指每一张画面。
需要注意帧率和刷新率不是一个概念:
帧率(frame rate)指的是显卡1秒钟渲染好并发送给显示器多少张画面。
刷新率指的是显示器逐行扫描刷新的速度。以 60 Hz 刷新率的屏幕为例,就是1s会刷60帧,一帧需要1000 / 60 ,约等于16ms,这个速度快到普通人眼感受不到屏幕在扫描。
画面撕裂
画面撕裂的形成,简单点说就是显示器把两帧或两帧以上的数据同时显示在同一个画面的现象。就像这样:
屏幕刷新频率是固定的,通常是60Hz。比如在60Hz的屏幕下,每16.6ms从Buffer
取一帧数据并显示。理想情况下,GPU绘制完一帧,显示器显示一帧。
但现在显卡性能大幅提高,帧率太高出现画面撕裂。屏幕刷新频率是固定的,通常是60Hz,如果显卡的输出高于60fps,两者不同步,画面便会显示撕裂的效果。其实,帧率太低也是会出现画面撕裂。
所以背后的本质问题就是,当刷新率和帧率不一致就会出现,就很容易出现画面撕裂现象。
拓展知识点:显卡与数据流动到显示屏过程
显卡主要负责把主机向显示器发出的显示信号转化为一般电器信号(数模转换),使得显示器能明白个人电脑在让它做什么。显卡的主要芯片叫“显示芯片”(Video chipset,也叫GPU或VPU,图形处理器或视觉处理器),是显卡的主要处理单元。显卡上也有和电脑存储器相似的存储器,称为“显示存储器”,简称显存。
数据离开CPU到达显示屏,中间经历比较关键的步骤:
1.从总线进入GPU:将CPU送来的数据送到北桥(简单理解成连接显卡等高速设备的),再送到GPU里面进行处理
2.将芯片处理完的数据送到显存。
3.从显存读取出数据再送到随机读写存储,数模转换器进行数模转换的工作(但是如果是DVI接口类型的显卡,直接输出数字信号)
4.从DAC进入显示器:将转换完的模拟信号送到显示屏
所以显卡很关键的作用是起数据处理和数模转换。
那么等显示器显示完再去绘制下一帧数据不就没有这个问题了吗?
这么简单一想好像是没问题。但问题关键就出在图像绘制和屏幕读取这一帧数据使用的是一块Buffer
。屏幕读取数据过程是无法确保这个Buffer
不会被修改。由于屏幕是逐行扫描,它不会被打断仍然会继续上一行的位置扫描,当出现Buffer
里有些数据根本没被显示器显示完就被重写了(即Buffer
里的数据是来自不同帧的混合),这样就出现了画面撕裂的现象。
双缓存
针对上面的问题关键:图像绘制和屏幕读取这一帧数据使用的是一块Buffer
可以想到的一种解决方案是:不让它们使用同一块Buffer
,用两块让它们各自为战不就好了,这么想的思路确实是对的。分析下这个具体过程:
当图像绘制和屏幕显示有各自的Buffer
后,GPU
将绘制完的一帧图像写入到后缓存(Back Buffer),显示器显示的时候只会去扫描前缓存的数据(Frame Buffer)
,在显示器未扫描完一帧前,前缓存区内数据不改变,屏幕就只会显示一帧的数据,避免了撕裂。
但这样做的最关键一步是,什么时候去交换两块Buffer
的数据?
等Back Buffer
准备完一帧数据就进行?这很明显是不可以的,这样就和只有一个缓存区的效果一样了,还是会出现撕裂现象。
根据逐行扫描的特性,当扫描完一个屏幕后,显示器会重新回到第一行进行下次的扫描,在这个间隙过程,屏幕没有在刷新,此时就是进行缓存区交换比较好的时机。
VBlank阶段和帧传递:
显示器扫描完一帧重新回到第一行的过程称为显示器的
VBlank
阶段。缓存区交换被称为
BufferSwap
,帧传递。
Andrid屏幕刷新机制的演变
VSync
那是谁控制这个缓冲区交换时机,或者说专业点,什么时机进行帧传递
呢?
这里就要提到VSync
了,它翻译过来叫垂直同步
,它会强制帧传递发生在显示器的VBlank阶段
。
需要注意的是:开启垂直同步后,就算显卡准备好了Back Buffer
的数据,但显示器没有逐行扫描完前缓冲区的,就不允许发生帧传递
。显卡就空载着,等待显示器扫描完毕后的VBlank阶段
。
这就解释了在玩游戏的时候,如果开启了垂直功能,游戏中显示的帧率一直处于一个帧率之下,这个显示帧率值就是屏幕刷新率。
那这个过程具体是怎么样的,真的就可以解决问题了?上面看着说的很有道理,但抽象到还是似懂非懂...
别急,下面就用几张图带你分析下具体的过程。
Jank
在下面的图中,你将会经常看到Jank
一词语,它术语翻译,叫做卡顿。卡顿很容易理解了,比如我们在打游戏时,经常会遇到同一帧画面在那显示很久没有变化,这就是所谓的Jank
场景1
先看下最原始的,只有双缓冲,没有VSync
影响下,它会发生什么:
图中Display
为显示屏, VSync
仅仅指双缓冲的交换。
(1)Display
显示第0帧,此时 CPU/GPU
渲染第1帧画面,并且在 Display
显示下一帧前完成。
(2)Display
正常渲染第一帧
(3)出于某种原因,如 CPU
资源被占用,系统没有及时处理第2帧数据,当 Display
显示下一帧时,由于数据没处理完,所以依然显示第1帧,即发生“Jank” ,
Jank术语翻译为卡顿,就是我们打游戏感受到的延迟。
上图出现的情况就是第2帧没有在显示前及时处理,导致屏幕多显示第一帧一次,导致后面的帧都延时了。根本原因是因为第2帧的数据没能在VBlank
时(即本次完成到下次扫描开始前的时间间隙)完成。
上图可以看到的是由于CPU资源被抢,导致第2帧的数据处理时机太晚,假设在双缓存交换完成后,CPU资源可以立刻为处理第二帧所用,就可以处理完成该帧的数据(当前前提是该帧的处理数据不超过刷新一帧的时间),也就避免了Jank的出现。
场景2
在双缓冲下,有了VSync
会怎么样呢?
如图,当且仅当收到VSync
通知(比如16ms触发一次),CPU
和GPU
立刻开始计算然后把数据写入Buffer
。VSync
同步信号的出现让绘制速度和屏幕刷新速度保持一致,使CPU
和GPU
充分利用了这16.6 ms的时间,减少了jank。
场景3
但是如果界面比较复杂,CPU/GPU处理时间真的超过16.6ms的话,就会发生:
图中可以看出当第1个 VSync
到来时GPU
还在处理数据,这时缓冲区在处理数据B,被占用了,此时的VBlank阶段
就无法进行缓冲区交换,屏幕依然显示前缓冲区的数据A,发生了jank
。当下一个信号到来时,此时 GPU 已经处理完了,那么就可以交换缓冲区,此时屏幕就会显示交互后缓冲区的数据B了。
由于硬件性能限制,我们无法改变 CPU/GPU 渲染的时间,所以第一次的Jank是无法避免的,但是在第二次信号来的时候,由于GPU占用了后缓冲区,没能实现缓冲区交换,导致屏幕依然显示上一帧A。由于此时,后缓冲区被占用了,就算此时CPU是空闲的也不能处理下一帧数据。增大了后期Jank的概率,比如图中第二个Jank的出现。
出现该问题本质的原因是,两个缓冲区各自被GPU/CPU、屏幕显示所占用。导致下一帧的数据不能被处理。
三缓存
找到问题的本质了,那很容易想到,再加一个Buffer
(这里叫它中Buffer
)参与,让添加的这个中Buffer
和后Buffer
交换,这样既不会影响到显示器读取前Buffer
,又可以在后Buffer
缓冲区不能处理时,让中Buffer
来处理。像下图这样:
当第一个信号到来时,前缓冲区在显示A、后缓冲区在处理B,它们都被占用。此时 CPU 就可以使用中缓冲区,来处理下一帧数据C。这样的话,C数据可以提前处理完成,之前第二次发生的Jank
就不存在了,有效的降低了Jank出现的几率。
到这里,可以看出,不管是双缓冲和三缓冲,都会有卡顿、延时问题,只是三缓冲下,减少了卡顿的次数。
那是不是 Buffer
越多越好呢?
答案是否定的,Buffer
存储的缓存数据是占有内存的,Buffer
越多,缓存数据就越多,内存占用就会增大,所以Buffer
只要3个就足够了。
Choreographer
那么在Android App层面,呈现在我们眼前的视觉效果(比如动画)是怎么出来的?是否和上述介绍的屏幕刷新机制呼应?或者说,它是怎么基于这个刷新机制原理实现的UI刷新?
对UI绘制流程熟悉的都知道,UI绘制会先走到ViewRootImpl#scheduleTraversals()
,之后才会执行UI绘制。
#ViewRootImpl
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//重点关注这里:绘制的操作封装在mTraversalRunnable里,交给`Choreographer`类处理
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//...
}
}
重点关注mChoreographer.postCallback(..)
,UI绘制的操作被封装在mTraversalRunnable
里,交由mChoreographer
的postCallback
方法处理。
mChoreographer
是Choreographer
对象。那Choreographer
类是做啥的呢,翻译为编舞者。这个类的命名很有意思,直接意思感觉和绘制毫无关联。但一只舞蹈的节奏控制是由编舞者掌控,就像绘制的过程的时机也需要类似这样一个角色控制一般。可见这个类的作者应该很喜欢舞蹈吧~
走入mChoreographer.postCallback
看看做了什么
#Choreographer
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {
//...
postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}
真正做事的是postCallbackDelayedInternal
#Choreographer
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
//把当前的runnable加入到callback队列中
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
//达到期限时间
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
如果这个任务达到约定的延时时间,那么就会直接执行scheduleFrameLocked
方法,如果没有达到就通过Handler
发送一个延时异步消息,最终也会走到scheduleFrameLocked
方法:
#Choreographer
//默认使用VSync同步机制
private static final boolean USE_VSYNC = SystemProperties.getBoolean(
"debug.choreographer.vsync", true);
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
//是否使用VSync同步机制
if (USE_VSYNC) {
//是否在主线程
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
scheduleFrameLocked()
会根据是否是使用VSync同步机制
,来执行不同的逻辑。下面顺着使用同步的情况分析:
判断当前线程的Looper
是否是创建Choreographer
时的线程Looper
,由于是在ViewRootImpl
中传入的,正常情况它是在主线程,所以就等价于判断当前线程是否在主线程。
如果不是就把这个消息加入到主线程,不管如何,最后都会走到scheduleVsyncLocked
方法:
#Choreographer
private final FrameDisplayEventReceiver mDisplayEventReceiver;
private void scheduleVsyncLocked() {
//调用DisplayEventReceiver的scheduleVsync
mDisplayEventReceiver.scheduleVsync();
}
mDisplayEventReceiver
是FrameDisplayEventReceiver
的对象。而FrameDisplayEventReceiver
继承了DisplayEventReceiver
这个抽象类。
DisplayEventReceiver
如它的命名一样直观,显示事件的接收者。在DisplayEventReceiver
的构造方法里面,会调用native方法nativeInit
初始化一个接收者。在scheduleVsync
方法里面,会调用native方法nativeScheduleVsync
,把初始化的接收者对象传进去。
#DisplayEventReceiver
public abstract class DisplayEventReceiver {
public DisplayEventReceiver(Looper looper, int vsyncSource) {
//初始化一个接收者
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
vsyncSource);
}
public void scheduleVsync() {
//初始化的接收者对象mReceiverPtr传进去
nativeScheduleVsync(mReceiverPtr);
}
}
FrameDisplayEventReceiver
比DisplayEventReceiver
更具体一点,叫做帧显示的事件接收者。在前面介绍过,当收到同步信号过来后,就希望显示下一帧数据。那是怎么接收同步信号的呢?魔法就在上述那两个native方法里面,调用这两个方法之后。就会接收到`onVsync'方法的回调。这就是同步信号到来的时机。
#Choreographer.FrameDisplayEventReceiver
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
//...
long now = System.nanoTime();
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
//timestampNanos / TimeUtils.NANOS_PER_MS 时间后走run方法
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
//接收到同步信号后执行
doFrame(mTimestampNanos, mFrame);
}
}
在onVsync
里,主要做的一件事就是在发送一个延时消息,时间是同步信号的时间戳,因为这个类是一个Runnable
,这个消息会在run方法里面处理,之后就会执行doFrame()
方法。
doFrame()
从它的命名,十有八九就是我们一直提的接收到VSync同步信号后,处理帧数据的地方了:
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
//抖动时间: 当前时间 - 同步信号通知的时间
final long jitterNanos = startNanos - frameTimeNanos;
//mFrameIntervalNanos = (long)(1000000000 / getRefreshRate()) 类似 1s/60hz = 16.6ms,不过这里是纳秒为单位
//抖动时间超过了一帧刷新的时间,即发生了Jank
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
//计算调帧数,超过一定限制(默认30),就表示应用在主线程做了大量工作,影响了绘制,打印提示
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
frameTimeNanos = startNanos - lastFrameOffset;
}
//...
}
try {
//按顺序执行任务(这里只留了核心代码)
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {}
}
在doFrame的最后,按顺序根据CallBack的类型执行任务,和我们在本节最开始的ViewRootImpl的这部分代码,关连起来了。我们post的这个类型是 Choreographer.CALLBACK_TRAVERSAL
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
终于快结束的节奏了,看看doCallbacks是做什么的
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
final long now = System.nanoTime();
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
try {
for (CallbackRecord c = callbacks; c != null; c = c.next) {
//注意这里:执行CallbackRecord的run方法
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
//回收处理完的CallbackRecord
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
}
}
CallbackRecord
是记录callBack信息的类,它是个链表结构,具有next
指针。它记录了callback
所要执行任务或者说行为,比如Runnbable
或者FrameCallback
。
private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action; // Runnable or FrameCallback
public Object token;
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
//执行我们最初post的Runnable
((Runnable)action).run();
}
}
}
对应我们最开始的postCallback
方法,这个action
也就是我们的mTraversalRunnable
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
到这里,postCallback
的操作形成一个完整的闭环。关于Choreographer
的介绍也就算完了。
总结
最后,我想分享一下本文的构思过程:
1.以屏幕显示的基础概念谈起,了解屏幕上的像素点是怎么显示出来的,对后面屏幕刷新的理解会变得更容易。
2.分析Android屏幕刷新机制
的演变过程,更轻松的理解目前的刷新机制是怎么出来的,为什么要有双缓冲、三缓冲。
3.从ViewRootImpl
的触发绘制为开始,到Choreographer
的doCallbacks
结束,形成了完整的闭环。通过对这部分源码的分析,看到Choreographer
这个编舞者是如何利用VSync同步机制
,来掌控整个UI的刷新过程。