深入浅出Android屏幕刷新原理
目录
前言
现在Android的应用界面越来越复杂,很多时候页面中还有各种动画,所以页面卡顿、掉帧等问题就随之而来,所以就想研究一下屏幕刷新的原理,以便于更快的定位和解决问题
基本概念
Android的屏幕刷新中涉及到最重要的三个概念(为便于理解,这里先做简单介绍)
CPU:执行应用层的measure、layout、draw等操作,绘制完成后将数据提交给GPU
GPU:进一步处理数据,并将数据缓存起来
屏幕:由一个个像素点组成,以固定的频率(16.6ms,即1秒60帧)从缓冲区中取出数据来填充像素点
总结一句话就是:CPU 绘制后提交数据、GPU 进一步处理和缓存数据、最后屏幕从缓冲区中读取数据并显示
我们开发过程中主要关心CPU绘制部分,对GPU和屏幕基本不用关心。所以,看到这里,有的人可能就会想说,我对view的绘制流程(measure、layout、draw)已经非常熟悉,至于GPU和屏幕,和我也没有太大关系吧。其实这里面还有更多的细节值得我们去探索,了解和掌握了这些细节,有助于我们解决一些实际开发过程中的问题,我们不妨一步步往下看
双缓冲机制
看完上面的流程图,我们很容易想到一个问题,屏幕是以16.6ms的固定频率进行刷新的,但是我们应用层触发绘制的时机是完全随机的(比如我们随时都可以触摸屏幕触发绘制),如果在GPU向缓冲区写入数据的同时,屏幕也在向缓冲区读取数据,会发生什么情况呢?有可能屏幕上就会出现一部分是前一帧的画面,一部分是另一帧的画面,这显然是无法接受的,那怎么解决这个问题呢?
这个其实和我们平时使用代码管理工具Git的一些思路有相似之处,首先我们有一个master分支,对应线上版本的代码,当有新的需求来的时候,我们往往不会在master分支上直接进行开发,都会拉出一个新的分支,比如develop分支,在develop分支上开发新需求,等开发完成测试通过后才会合并到master分支
所以,在屏幕刷新中,Android系统引入了双缓冲机制。GPU只向Back Buffer中写入绘制数据,且GPU会定期交换Back Buffer和Frame Buffer,也就是让Back Buffer 变成Frame Buffer交给屏幕进行绘制,让原先的Frame Buffer变成Back Buffer进行数据写入。交换的频率也是60次/秒,这就与屏幕的刷新频率保持了同步
虽然我们引入了双缓冲机制,但是我们知道,当布局比较复杂,或设备性能较差的时候,CPU并不能保证在16.6ms内就完成绘制数据的计算,所以这里系统又做了一个处理
当你的应用正在往Back Buffer中填充数据时,系统会将Back Buffer锁定。如果到了GPU交换两个Buffer的时间点,你的应用还在往Back Buffer中填充数据,GPU会发现Back Buffer被锁定了,它会放弃这次交换
这样做的后果就是手机屏幕仍然显示原先的图像,这就是我们常常说的丢帧,所以为了避免丢帧的发生,我们就要尽量减少布局层级,减少不必要的View的invalidate调用,减少大量对象的创建(GC也会占用CPU时间)等等。对这方面有兴趣的可以看我的性能优化专题下的文章
Choreographer
我们看下面这张图,这里已经是基于双缓冲机制,且应用层的优化已经做得非常好,绘制时间均少于16.6ms,但依然出现了丢帧,为什么呢?
原因是第2帧虽然绘制时间少于16.6ms,但是绘制开始的时间距离vsync信号(就是一个发起屏幕刷新的信号)发出的时间比较短暂,导致当vsync信号来的时候,第2帧还没有绘制完成,所以Back Buffer依然是锁定的状态,也就出现了丢帧
如果我们可以保证每次绘制开始的时间和vsync信号发起的时间一致(如下图所示),是不是就可以解决这个问题呢?
Android在每一帧中实际上只是在完成三个操作,分别是输入(Input)、动画(Animation)、绘制(Draw)。在Android4.1(API 16)之后,Android系统开始加入Choreographer
这个类,这个类名翻译过来是“舞蹈指导”,字面上的意思就是指挥以上三个UI操作一起完成一支舞蹈。这个类就可以解决vsync和绘制不同步的问题,其实它的原理用一句话总结就是往Choreographer里发一个消息,最快也要等到下一个vsync信号来的时候才会开始处理消息
下面我们通过源码分析来看看Choreographer
的实现原理
Activity中的布局首次绘制,以及每次调用View 的 invalidate()
时,都会调用到ViewRootImp#requestLayout()
,对于这块不是很清楚的具体可以看最全的View绘制流程(上)— Window、DecorView、ViewRootImp的关系,所以我们接下来分析一下ViewRootImp#requestLayout()
里面做了什么
ViewRootImp#requestLayout()
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//检查是否是主线程,不然会抛出异常
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
ViewRootImp#scheduleTraversals()
如果在同一帧中出现多次requestLayout()
调用,其实最终也只会绘制一次,为什么呢?我们可以看到下面有个mTraversalScheduled
标志位,稍后我们可以看看这个标志位是哪里被置为false的
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//添加同步消息屏障,这个方法也比较关键,这里先不关心,我们说完Choreographer再分析
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//向Choreographer中发送消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//...
}
}
Choreographer#postCallbackDelayedInternal()
mChoreographer.postCallback()
接着会调用这个方法
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
//将消息以当前的时间戳放进mCallbackQueue 队列里
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
//如果没有设置消息延时,直接执行
scheduleFrameLocked(now);
} else {
//消息延时,但是最终依然会调用scheduleFrameLocked
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
Choreographer#scheduleFrameLocked()
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
if (isRunningOnLooperThreadLocked()) {
//如果当前线程是Choreographer的工作线程,我理解就是主线程
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);
}
}
}
接下来最终会调用到一个native方法
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}
public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
nativeScheduleVsync(mReceiverPtr);
}
}
@FastNative
private static native void nativeScheduleVsync(long receiverPtr);
native方法我们在Android Studio中不能直接查看,这里我们换一种思路。前面Choreographer#postCallbackDelayedInternal()
方法中,我们看到了将消息以当前的时间戳放进队列里,那消息什么时候被取出来执行呢?
CallbackQueue
private final class CallbackQueue {
private CallbackRecord mHead;
public boolean hasDueCallbacksLocked(long now) {
return mHead != null && mHead.dueTime <= now;
}
//这就是取出消息的方法
public CallbackRecord extractDueCallbacksLocked(long now) {
CallbackRecord callbacks = mHead;
if (callbacks == null || callbacks.dueTime > now) {
return null;
}
CallbackRecord last = callbacks;
CallbackRecord next = last.next;
while (next != null) {
if (next.dueTime > now) {
last.next = null;
break;
}
last = next;
next = next.next;
}
mHead = next;
return callbacks;
}
//添加消息
public void addCallbackLocked(long dueTime, Object action, Object token) {...}
//删除消息
public void removeCallbacksLocked(Object action, Object token) {...}
}
跟踪代码发现,这个CallbackQueue#extractDueCallbacksLocked()
会被Choreographer#doCallbacks()
调用,Choreographer#doCallbacks()
又会被Choreographer#doFrame()
调用,最终我们跟到了FrameDisplayEventReceiver
类
FrameDisplayEventReceiver
因为上面的native方法我们没有跟进去分析,担心给大家绕晕了,我们会用一个新的章节来分析native层做的事情,这里先直接给出结论
nativeScheduleVsync()
会向SurfaceFlinger
注册Vsync信号的监听,VSync信号由SurfaceFlinger
实现并定时发送,当Vsync信号来的时候就会回调FrameDisplayEventReceiver#onVsync()
,这个方法给发送一个带时间戳Runnable
消息,这个Runnable
消息的run()
实现就是FrameDisplayEventReceiver# run()
, 接着就会执行doFrame()
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
scheduleVsync();
return;
}
long now = System.nanoTime();
if (timestampNanos > now) {
timestampNanos = now;
}
if (mHavePendingVsync) {
} else {
mHavePendingVsync = true;
}
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
//设置异步消息
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}
doFrame()
会计算当前时间与时间戳的间隔,间隔越大表示这一帧处理的时间越久,如果间隔超过一个周期,就会去计算跳过了多少帧,并打印出一个日志,这个日志我想很多人可能都见过
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
最终doFrame()
会从mCallbackQueue
中取出消息并按照时间戳顺序调用mTraversalRunnable
的run()
函数,mTraversalRunnable
就是最初被加入到Choreographer
中的Runnable()
//ViewRootImp
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
TraversalRunnable
doTraversal()
中就会开始我们View的绘制流程,View的绘制流程不是本文的重点,感兴趣的可以看最全的View绘制流程(下)— Measure、Layout、Draw
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
ViewRootImp#doTraversal()
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步消息屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
到此为止,从触发绘制到屏幕真正开始绘制的过程就基本讲完了,但是这里还有最后一个细节没有进行分析
同步消息屏障
还记不记得前面说有mHandler.getLooper().getQueue().postSyncBarrier()
这个方法还没有进行分析,这个方法的作用是什么呢?
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//☆
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//向Choreographer中发送消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//...
}
}
我们知道,Android是基于消息机制的,每一个操作都是一个Message
,如果在触发绘制的时候,消息队列中还有很多消息没有被执行,那是不是意味着要等到消息队列中的消息执行完成后,绘制消息才能被执行到,那么依然无法保证Vsync信号和绘制的同步,所以依然可能出现丢帧的现象
还记不记得我们之前在Choreographer#scheduleFrameLocked()
和FrameDisplayEventReceiver#onVsync()
中提到,我们会给与Message
有关的绘制请求设置成异步消息(msg.setAsynchronous(true)
),为什么要这么做呢?这时候MessageQueue#postSyncBarrier()
就发挥它的作用了,简单来说,它的作用就是一个同步消息屏障
MessageQueue#postSyncBarrier()
主线程的 Looper
会一直循环调用 MessageQueue
的 next()
来取出队头的 Message
执行,当 Message
执行完后再去取下一个。当 next()
方法在取 Message
时发现队头是一个同步屏障的消息时,就会去遍历整个队列,只寻找设置了异步标志的消息,如果有找到异步消息,那么就取出这个异步消息来执行,否则就让 next()
方法陷入阻塞状态。如果 next()
方法陷入阻塞状态,那么主线程此时就是处于空闲状态的,也就是没在干任何事。所以,如果队头是一个同步屏障的消息的话,那么在它后面的所有同步消息就都被拦截住了,直到这个同步屏障消息被移除,否则主线程就一直不会去处理同步屏障后面的同步消息
那这么同步屏障是什么时候被移除的呢?
其实我们就是在我们上面提到的ViewRootImp#doTraversal()
方法中
总结
本文讲了屏幕刷新的基本原理,以及双缓冲机制、Choreographer的作用、同步消息屏障,不同的地方出了问题都可能引起丢帧,所以了解这些细节有助于我们更好的排查项目开发过程中的问题,最后,来梳理一下屏幕刷新的流程图