最近的冠状病毒搞得人心惶惶,待在家里面也没啥事,正好趁这段时间学习一下。了解一下Android屏幕的刷新机制对于我们解决应用卡顿也有帮助。
1.屏幕刷新机制
1)屏幕显示
首先我们看下Android屏幕的显示原理,如下图所示
应用需要界面重绘时,会向系统申请buffer,拿到buffer之后会将图像信息写入buffer当中提交给系统,然后屏幕从缓冲区拿到图像数据进行显示。
Android手机一般都是60帧/秒,也就是每16ms屏幕从缓冲区读取图像数据进行刷新,如果没有新的图像数据更新,则会一直读到旧的图像数据,显示旧的图像。
2)VSync信号
屏幕都是周期性的刷新,每个周期,系统均会产生一个VSync信号,这样就会产生一个问题,如下图所示
应用绘制完第一帧,恰好在VSync周期内,然后在下一个周期,屏幕就能够显示第一帧;
但是第二帧绘制的时间处于两个VSync周期之间,所以当第二帧绘制完成之后,只有在第四个周期才能够显示,然后导致第三帧只能在第五个周期显示,然而第三帧在第三个周期内就已经绘制完成。这样就会导致应用的卡顿。
所以为了解决这个问题,在VSync信号来临的时候才进行绘制,如下图所示,让应用的绘制和屏幕的显示保持同步。
而实现保持同步功能的类就是Choreographer。
2.Choreographer
首先我们来看下ViewRootImpl.requestLayout,应用端请求刷新界面。
我们可以看到在注释2处,增加了一个消息屏障,在屏障之后的所有普通消息都暂时不能够执行,只有撤除屏障之后才能够执行,但是异步消息不受屏障的影响,这样做的好处就是让真正重要的消息能够优先执行。
另外我们需要注意的是,在注释1处,判断了mTraversalScheduled为false,才会post异步消息,而一旦执行post,mTraversalScheduled就被置为true,而只有当下一个VSync信号来临之时,执行了mTraversalRunnable,mTraversalScheduled才能重新被设置为false。所以如果应用端同时发起多次requestLayout,在一个VSync周期内,也只能执行一次绘制。
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
//1.mTraversalScheduled为false才执行
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//2.增加一个消息屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//3.post了一个异步消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
接着我们来看下postCallback,最终会执行到postCallbackDelayedInternal,先看注释1,mCallbackQueues是一个单链表数组,根据callbacyType,将action插入到链表当中。
我们传入的delayMills为0,所以会执行到注释2处。
注释3处,如果在当前线程,则直接执行scheduleVsyncLocked,否则使用handler post到指定的线程执行,scheduleVsyncLocked的作用就是告诉SurfaceFlinger,当下一个VSync信号来临的时候,我们能够收到回调。
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
......
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
//1.单链表,根据callbackType将action插入到链表当中
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
//2.执行scheduleFrameLocked
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
private void scheduleFrameLocked(long now) {
......
//3.如果在当前线程,则直接执行scheduleVsyncLocked,否则发送msg到对应的线程执行scheduleVsyncLocked
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
......
}
private void scheduleVsyncLocked() {
//4.当VSync信号来临时,mDisplayEventReceiver能够收到回调
mDisplayEventReceiver.scheduleVsync();
}
然后我们来看下回调的地方,当VSync信号来临的时候,会回调到onVsync函数里面,然后封装一个msg,使用handler发出,最终会执行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) {
......
mTimestampNanos = timestampNanos;
mFrame = frame;
//1.将this传入,即Runnable,msg为一个异步消息
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都干了些啥
首先看下在注释1处,用当前时间减去帧的时间得到延时时间,并用延时时间除以帧的周期,如果超过了规定的常量,就会打印“The application may be doing too much work on its main thread.”,表明应用在主线程做了太多的工作,导致跳了太多帧,需要优化。
注释2处显示了处理各种类型的callback,其中frameTimeNanos是帧的时间戳,只有到了时间的callback才能够被处理。
void doFrame(long frameTimeNanos, int frame) {
......
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
//1.当前时间-帧时间表示延时的时间
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
......
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
//2.处理callback
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
......
}
然后我们再来看下之前传入的callback是啥,如下所示是CALLBACK_TRAVERSAL类型的,
而mTranversalRunnable是一个Runnable,最终会执行doTraversal函数,其中performTraversals就是真正开始界面的绘制。
void scheduleTraversals() {
//1.mTraversalScheduled为false才执行
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//2.增加一个消息屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//3.post了一个异步消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//4.真正的绘制界面
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
3.总结
绕来绕去是不是都有点儿晕了,其实弄懂之后流程也不是特别复杂,以下是流程图可以帮助理解