Android屏幕的刷新机制
最近的冠状病毒搞得人心惶惶,待在家里面也没啥事,正好趁这段时间学习一下。了解一下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.总结
绕来绕去是不是都有点儿晕了,其实弄懂之后流程也不是特别复杂,以下是流程图可以帮助理解