最近的冠狀病毒搞得人心惶惶,待在家裏面也沒啥事,正好趁這段時間學習一下。瞭解一下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.總結
繞來繞去是不是都有點兒暈了,其實弄懂之後流程也不是特別複雜,以下是流程圖可以幫助理解