接上回,今天说说VSync
机制的相关处理,也就是Choreographer
类。
每次说到源码就很难表述,所以今天还是通过问题的方式,一步步解析这个“编舞者”
。
(本篇涉及到大量Handler知识点,如果忘记的朋友可以再翻翻我之前写的《Handler27问》- https://juejin.cn/post/6943048240291905549
)
界面有绘制任务时候,会马上执行performTraversals吗?
当界面有绘制任务,一般是执行了两种方法:
View.invalidate
View.requestLayout
而这两个方法的的终点都是ViewRootImp类的scheduleTraversals
方法,开始View树的绘制任务。
我们就从这里入手:
代码语言:javascript复制 void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
主要做了三件事:
- 设置了
mTraversalScheduled
标识位为true。这个标示位的用法,很像我们防止按钮多次点击时候的逻辑,所以我们可以猜测下它就是为了防止某个时间段多次绘制任务的提交。 - 给主线程的Handler加入了
同步屏障
。同步屏障我们之前说过了,它的存在是为了保证后续的异步消息优先执行。 - 最后就是用到我们的
Choreographer
类,调用了postCallback
方法。
我们先不看这个postCallback
方法,而是看看传入的一个参数mTraversalRunnable
。
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
performTraversals();
}
}
可以发现,在这个Runnable中执行了doTraversal
,同样是三件事:
- 设置
mTraversalScheduled
标识位为false。 - 移除
同步屏障
。 - 执行
performTraversals
方法,开始绘制。
这不巧了吗,刚好对应了刚才的三件事。
也就是在这里,执行了performTraversals
方法开始绘制View树,即layout、measure、draw
。
所以这个标识位
我们可以断定了,就是防止在开始绘制之前的某个时间段内多次提交绘制需求。而且在这里移除了同步屏障,因为绘制任务已经在执行了,不用怕别的Handler消息任务来插队了。
那这个过程到底发生了什么呢?是马上执行了doTraversal
方法吗?悬念自然就在postCallback
方法中了,继续下一个问题。
Choreographer单例是怎么实现的?保存在哪里?
刚才我们看到了编舞者Choreographer
的身影,那么它又是从哪里来的呢?
public ViewRootImpl(Context context, Display display) {
//...
mChoreographer = Choreographer.getInstance();
}
//Choreographer.java
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
Looper looper = Looper.myLooper();
if (looper == null) {
throw new IllegalStateException("The current thread must have a looper!");
}
Choreographer choreographer = new Choreographer(looper, VSync_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
mMainInstance = choreographer;
}
return choreographer;
}
};
public static Choreographer getInstance() {
return sThreadInstance.get();
}
在ViewRootImpl
构造方法中获取了Choreographer
实例,至于怎么获取的。。没错,又是它:ThreadLocal
。
细节就不说了,ThreadLocal
主要是保证一个线程对应一个实例,Choreographer
一般只存在于主线程,所以就是用于单例。
Choreographer怎么处理绘制任务呢?
好了,接着上面的说说postCallback
方法,也就是绘制任务被交给Choreographer
之后是怎么处理的呢?
public void postCallback() {
postCallbackDelayed(callbackType, action, token, 0);
}
public void postCallbackDelayed() {
postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}
private void postCallbackDelayedInternal() {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
//如果没有延时,马上预定下一个VSync信号
scheduleFrameLocked(now);
} else {
//会在任务执行时间去预定VSync信号
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
- 首先,看到
delayMillis
,就知道这个任务是可以延迟的,跟Handler很像,会把延迟后的时间记录下来,也就是任务的执行时间。 - 然后,把这个任务带上执行时间加到了
mCallbackQueues
数组 - 最后,判断了任务执行时间,如果没有延时就马上执行
scheduleFrameLocked
方法,否则就通过Handler延时执行任务。
继续看scheduleFrameLocked
方法:
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSync) {
//开启了VSync
if (isRunningOnLooperThreadLocked()) {
scheduleVSyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSync);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
//没有开启VSync
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS sFrameDelay, now);
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
- 如果开启了
VSync
,会判断当前是否运行在主线程,如果不是主线程就通过Handler切换线程,执行MSG_DO_SCHEDULE_VSync
消息。 - 如果在主线程,就执行
scheduleVSyncLocked
方法,所以我们猜测MSG_DO_SCHEDULE_VSync
消息应该也是执行这个方法。 - 如果没有开启
VSync
,就会通过Handler执行MSG_DO_FRAME
消息。
到此,我们知道了Choreographer
把任务加到数组之后,就开始根据VSync执行了scheduleVSyncLocked
方法,所以这个方法自然跟VSync也就是垂直同步信号有关了,继续看看。
Choreographer与VSync信号的关系?每次VSync信号都能接收到吗?
代码语言:javascript复制 private void scheduleVSyncLocked() {
mDisplayEventReceiver.scheduleVSync();
}
public void scheduleVSync() {
if (mReceiverPtr == 0) {
} else {
nativeScheduleVSync(mReceiverPtr);
}
}
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
@Override
public void onVSync(long timestampNanos, long physicalDisplayId, int frame) {
//...
}
@Override
public void run() {
//...
}
}
哎呀,这就没了?
所以绘制任务传到Choreographer
之后,最终调用的是native方法nativeScheduleVSync
。
这个底层方法就不去细究了,它的目的就是注册VSync
信号的监听。
nativeScheduleVSync() 会向 SurfaceFlinger 注册VSync信号的监听,VSync信号由 SurfaceFlinger 实 现并定时发送,当VSync信号来的时候就会回调 FrameDisplayEventReceiver#onVSync()
所以,当有绘制任务的时候,Choreographer
会去注册VSync信号的监听,注册之后,下一个垂直同步信号到来的时候,才会传到Choreographer中的FrameDisplayEventReceiver#onVSync()
方法。
也就是说,不需要绘制的时候,VSync信号是不会发到Choreographer这里的。虽然每隔16.6ms,就有一次VSync信号,但是只有需要绘制的时候,才会去订阅VSync信号,然后才开始真正的绘制工作。
好像剧透了?还没说到绘制工作呢。。
VSync信号到来的时候,Choreographer会怎么操作?
接下来就看看垂直同步信号来的时候,Choreographer
做了啥。刚才说到会执行FrameDisplayEventReceiver#onVSync()
方法。
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
@Override
public void onVSync(long timestampNanos, long physicalDisplayId, int frame) {
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
doFrame(mTimestampNanos, mFrame);
}
}
这里要注意的是,这里的Runnable
是包装成为Handler中的异步消息加到了消息队列中,而这个Handler也是主线程的Handler,待会会细说。
而Runnable
中的run方法,会执行到doFrame方法,这个doFrame我们就可以理解为这一帧要做的事情。
到这里,VSync信号机制其实就显现出来了,正常情况下,每隔16.6ms系统就会发送一次VSync信号。
- 如果有绘制任务,就会通过
Choreographer
类进行下一次信号的监听 - 当垂直同步信号来了,就会切换到主线程执行
doFrame
方法开始下一帧数据的绘制,也就是之前所说的CPU工作的开始。 - 没有任务的情况下,VSync信号就不会发到
Choreographer
这里。
来个图:
最后看看doFrame
方法:
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
//VSync信号时间
long intendedFrameTimeNanos = frameTimeNanos;
//当前时间
startNanos = System.nanoTime();
//时间差
final long jitterNanos = startNanos - frameTimeNanos;
//如果当前时间大于Vsync信号时间
if (jitterNanos >= mFrameIntervalNanos) {
//就设置这一帧的时间为上一个Vsync信号时间
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
frameTimeNanos = startNanos - lastFrameOffset;
}
//如果这一帧时间小于上次帧绘制时间,就重新申请新的VSync信号
if (frameTimeNanos < mLastFrameTimeNanos) {
scheduleVsyncLocked();
return;
}
}
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);
}
}
中间的一系列时间判断其实是对时间做校正,暂且按下不表,看看最后执行任务的时候干了啥,也就是doCallbacks
方法。
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);
try {
for (CallbackRecord c = callbacks; c != null; c = c.next) {
c.run(frameTimeNanos);
}
}
}
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
((Runnable)action).run();
}
}
粗略的看下,可以得知其实是执行到任务中action的run方法,这个action很明显就是当初传进来的Runnable
,所以最后就是执行到doTraversal
方法,开始三大绘制流程。
终于,一个环闭合了,当然中间还有很多细节,我们慢慢看。
Choreographer内部的Handler是干什么的?
刚才说到很多次Choreographer
内部的Handler,来看看它是何方神圣:
private Choreographer(Looper looper, int vsyncSource) {
mHandler = new FrameHandler(looper);
}
private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
//绘制新的一帧数据
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
//申请VSYNC信号
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
//延迟预定VSync信号
doScheduleCallback(msg.arg1);
break;
}
}
}
Choreographer
的单例存在于主线程,所以这里的FrameHandler
也是主线程的Handler。
所以这里的Handler
有两个作用:
- 1、作为切换到主线程,比如
MSG_DO_FRAME
消息。 - 2、
延时消息
,比如之前说到的延迟预定VSync信号,发的就是MSG_DO_SCHEDULE_CALLBACK消息。
Choreographer中保存任务的数组结构是如何?
还记得刚才添加绘制任务的时候吗?调用了addCallbackLocked
方法:
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
进去看看里面的任务处理:
代码语言:javascript复制 mCallbackQueues = new CallbackQueue[CALLBACK_LAST 1];
for (int i = 0; i <= CALLBACK_LAST; i ) {
mCallbackQueues[i] = new CallbackQueue();
}
private final class CallbackQueue {
//链表
private CallbackRecord mHead;
//添加链表节点
public void addCallbackLocked(long dueTime, Object action, Object token) {
CallbackRecord callback = obtainCallbackLocked(dueTime, action, token);
CallbackRecord entry = mHead;
if (entry == null) {
mHead = callback;
return;
}
//插入链表头部
if (dueTime < entry.dueTime) {
callback.next = entry;
mHead = callback;
return;
}
//找到合适的时间点插入
while (entry.next != null) {
if (dueTime < entry.next.dueTime) {
callback.next = entry.next;
break;
}
entry = entry.next;
}
entry.next = callback;
}
}
也?又是链表嗦。
没错,数组里面存储的是CallbackQueue
对象,而CallbackQueue
对象里面存储的就是链表。
那数组的序号callbackType
又是什么呢?
//输入任务
public static final int CALLBACK_INPUT = 0;
//动画任务
public static final int CALLBACK_ANIMATION = 1;
//插入更新动画任务
public static final int CALLBACK_INSETS_ANIMATION = 2;
//绘制任务
public static final int CALLBACK_TRAVERSAL = 3;
//提交任务
public static final int CALLBACK_COMMIT = 4;
而我们的view树绘制任务传入的就是CALLBACK_TRAVERSAL
。
到这里,Choreographer
中的数组结构我们算是搞清楚了,数组一共有五个对象,分别对应着每个任务类别,而每个对象都存储着一个任务链表,记录了任务的回调和时间。
而最后处理的时候,也是按照链表上元素的时间进行处理:
代码语言:javascript复制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;
}
通过遍历,把任务时间大于当前时间的任务都摘除掉,只剩下当前时间之前的所有任务,然后去遍历执行run方法。
其实这里我好奇的一点是,比如绘制任务CALLBACK_TRAVERSAL
,明明同一帧时间内,scheduleTraversals
只会执行一次。原因刚才说过了,通过mTraversalScheduled
字段控制的。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
}
}
那么,为什么这里要用链表结构呢?还遍历链表进行处理。
于是我去全局搜索了CALLBACK_TRAVERSAL
任务,结果还真发现了另外的地方也调用了:
//DisplayPowerState
private void scheduleColorFadeDraw() {
if (!mColorFadeDrawPending) {
mColorFadeDrawPending = true;
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL,
mColorFadeDrawRunnable, null);
}
}
DisplayPowerState
应该是控制电量相关View的。
虽然没有细看,但是说明界面的绘制应该不只是scheduleTraversals
一种情况的,有了解的朋友可以来讨论下。
哪些情况会导致掉帧呢(开发角度考虑)?Choreographer又是怎么处理的?
接下来就看看实际会发生掉帧的情况。
1、发起了一次绘制任务,然后会申请VSync信号,当信号来的时候,将doFrame消息加到主线程的消息队列中,这时候主线程正在被其他任务占用,并且迟迟不能结束。**
我们画个图表示下这种情况:
可以看到,在VSync1
之前我们就提交了绘制任务,按道理应该是VSync1
到来的时候执行doFrame方法,但是被耗时任务耽误了,等到耗时任务执行完已经是VSync3
之后的时间了,所以本该VSync2的时间点屏幕就可以显示第二张图片,结果被拖到了VSync4
的时间点才显示第二张图片,发生了掉帧情况。
这时候执行onFrame
会怎么处理呢?这就涉及到刚才没说到的时间校正了:
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
//VSync信号时间
long intendedFrameTimeNanos = frameTimeNanos;
//当前时间
startNanos = System.nanoTime();
//时间差
final long jitterNanos = startNanos - frameTimeNanos;
//如果当前时间大于Vsync信号时间
if (jitterNanos >= mFrameIntervalNanos) {
//就设置这一帧的时间为上一个Vsync信号时间
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
frameTimeNanos = startNanos - lastFrameOffset;
}
//如果这一帧时间小于上次帧绘制时间,就重新申请新的VSync信号
if (frameTimeNanos < mLastFrameTimeNanos) {
scheduleVsyncLocked();
return;
}
mLastFrameTimeNanos = frameTimeNanos;
}
}
进入doFrame
方法,frameTimeNanos
应该为VSync1
的时间,而当前时间是在VSync3
之后,假设为VSync3 5ms
。
所以时间差 jitterNanos = 两帧时间 5ms
,上一帧时间偏差 lastFrameOffset = 5ms
。
而这里做的校正就是把要绘制的这一帧时间frameTimeNanos
设置为最近一帧的时间,也就是VSync3
的时间,然后赋值给mLastFrameTimeNanos
。
校正之后,就能保证帧的处理时间与VSync信号的时间是在一个节奏上的,保证帧处理的连贯性。
2、View树绘制的时间过长。
也就是doTraversal
方法的执行时间过长,也就是doFrame
本身的执行时间过长,再来张图片说明:
正常情况下,doFrame
方法应该在一帧时间内,也就是16.6ms内完成绘制工作。但是如果绘制时间过长,跨过了几帧时间,那么新一帧的显示也就延后了,导致了掉帧情况。
这时候Choreographer
又是怎么处理的呢?
void doFrame(long frameTimeNanos, int frame) {
//...
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);
}
}
可以看到,doFrame方法中,执行了一系列任务之后,还执行了一个CALLBACK_COMMIT
任务,这个任务有点特殊,它包含了对doFrame方法执行完毕的一个处理:
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
if (callbackType == Choreographer.CALLBACK_COMMIT) {
final long jitterNanos = now - frameTimeNanos;
if (jitterNanos >= 2 * mFrameIntervalNanos) {
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos
mFrameIntervalNanos;
frameTimeNanos = now - lastFrameOffset;
mLastFrameTimeNanos = frameTimeNanos;
}
}
}
当任务为CALLBACK_COMMIT的时候,会再次判断当前时间和 绘制帧时间 的时间差,如果大于两个帧时间,就将绘制帧时间移动到 最近一个VSync时间点之前的一个VSync时间点,也就是倒数第二个VSync时间点。
从图中分析下:
从刚才的代码分析我们得知,frameTimeNanos
每次会被设置为倒数第一个垂直信号的时间,也就是VSync1时间点。
现在在每次doFrame
执行完毕后,如果发现doFrame耗时超过两帧时间,也就是图中黄色耗时,那么就把frameTimeNanos时间设置为倒数第二个垂直信号时间点,也就是VSync2时间点。
这又是为何呢?
还是为了保证帧处理时间的连贯性,同时又保证了帧时间的不重复。
例如在执行doFrame
耗时方法的时候(叫他doFrame1方法),VSync3之前又申请了一个绘制任务,那么当VSync3来的时候,就向Handler中加了新的一个doFrame消息,我们叫他doFrame2消息。
- 如果
doFrame1
方法执行完,不校正时间,那么帧时间frameTimeNanos
还是为VSync1
。而doFrame2执行的时候,帧时间为VSync3,就不连贯了。 - 如果
doFrame1
方法执行完,校正时间为倒数第一个帧时间,也就是为VSync3
。结果doFrame2执行的帧时间也是VSync3,就出现了重复的情况。
所以,考虑连贯性和不重复性,就设置为倒数第二个垂直信号的时间点为最后的帧时间。
可以通过代码统计掉帧情况吗?
可以,用到的是Choreographer的postFrameCallback方法。
代码语言:javascript复制Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
long starTime=System.nanoTime();
Log.e(TAG,"starTime=" starTime ", frameTimeNanos=" frameTimeNanos ", frameDueTime=" (frameTimeNanos-starTime)/1000000);
}
});
通过Choreographer
的回调方法,我们可以获取每一帧的获取时间。我们可以进去看看原理:
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}
private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action; // Runnable or FrameCallback
public Object token;
@UnsupportedAppUsage
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
((Runnable)action).run();
}
}
}
同样是发送了一个任务,不同的是token=FRAME_CALLBACK_TOKEN
。
所以当VSync信号来的时候,会执行回调方法doFrame,我们就可以在这里自己进行帧时间处理了。
总结:Choreographer是什么?
Choreographer
到底是什么呢?
- 是View绘制、动画等界面变动任务的接收和执行者。
- 是可以对VSync信号进行预约和响应的监听者
- 是同步VSync信号和绘制工作的编舞者。
参考
https://www.jianshu.com/p/0a54aa33ba7d https://juejin.cn/post/6864365886837686285
感谢大家的阅读,有一起学习的小伙伴可以关注下公众号—
码上积木
❤️ 每日一个知识点,建立完整体系架构。