Android的VSYNC機制和UI重新整理流程【金石計劃】

語言: CN / TW / HK

本文正在參加「金石計劃 . 瓜分6萬現金大獎」

前言

螢幕重新整理幀率不穩定,掉幀嚴重,無法保證每秒60幀,導致螢幕畫面撕裂;

今天我們來講解下VSYNC機制和UI重新整理流程

一、 Vsync訊號詳解

1、螢幕重新整理相關知識點

  • 螢幕重新整理頻率: 一秒內螢幕重新整理的次數(一秒內顯示了多少幀的影象),單位 Hz(赫茲),如常見的 60 Hz。重新整理頻率取決於硬體的固定引數(不會變的);
  • 逐行掃:顯示器並不是一次性將畫面顯示到螢幕上,而是從左到右邊,從上到下逐行掃描,順序顯示整屏的一個個畫素點,不過這一過程快到人眼無法察覺到變化。以 60 Hz 重新整理率的螢幕為例,這一過程即 1000 / 60 ≈ 16ms;
  • 幀率: 表示 GPU 在一秒內繪製操作的幀數,單位 fps。例如在電影界採用 24 幀的速度足夠使畫面執行的非常流暢。而 Android 系統則採用更加流程的 60 fps,即每秒鐘GPU最多繪製 60 幀畫面。幀率是動態變化的,例如當畫面靜止時,GPU 是沒有繪製操作的,螢幕重新整理的還是buffer中的資料,即GPU最後操作的幀資料;
  • 螢幕流暢度:即以每秒60幀(每幀16.6ms)的速度執行,也就是60fps,並且沒有任何延遲或者掉幀;
  • FPS:每秒的幀數;
  • 丟幀:在16.6ms完成工作卻因各種原因沒做完,佔了後n個16.6ms的時間,相當於丟了n幀;

2、VSYNC機制

VSync機制: Android系統每隔16ms發出VSYNC訊號,觸發對UI進行渲染,VSync是Vertical Synchronization(垂直同步)的縮寫,是一種在PC上很早就廣泛使用的技術,可以簡單的把它認為是一種定時中斷。而在Android 4.1(JB)中已經開始引入VSync機制;

VSync機制下的繪製過程;CPU/GPU接收vsync訊號,Vsync每16ms一次,那麼在每次發出Vsync命令時,CPU都會進行重新整理的操作。也就是在每個16ms的第一時間,CPU就會響應Vsync的命令,來進行資料重新整理的動作。CPU和GPU的重新整理時間,和Display的FPS是一致的。因為只有到發出Vsync命令的時候,CPU和GPU才會進行重新整理或顯示的動作。CPU/GPU接收vsync訊號提前準備下一幀要顯示的內容,所以能夠及時準備好每一幀的資料,保證畫面的流暢; 

可見vsync訊號沒有提醒CPU/GPU工作的情況下,在第一個16ms之內,一切正常。然而在第二個16ms之內,幾乎是在時間段的最後CPU才計算出了資料,交給了Graphics Driver,導致GPU也是在第二段的末尾時間才進行了繪製,整個動作延後到了第三段內。從而影響了下一個畫面的繪製。這時會出現Jank(閃爍,可以理解為卡頓或者停頓)。這時候CPU和GPU可能被其他操作佔用了,這就是卡頓出現的原因;

二、UI重新整理原理流程

1、VSYNC流程示意

當我們通過setText改變TextView內容後,UI介面不會立刻改變,APP端會先向VSYNC服務請求,等到下一次VSYNC訊號觸發後,APP端的UI才真的開始重新整理,基本流程如下:

image.png

setText最終呼叫invalidate申請重繪,最後會通過ViewParent遞迴到ViewRootImpl的invalidate,請求VSYNC,在請求VSYNC的時候,會新增一個同步柵欄,防止UI執行緒中同步訊息執行,這樣做為了加快VSYNC的響應速度,如果不設定,VSYNC到來的時候,正在執行一個同步訊息;

2、view的invalidate

View會遞迴的呼叫父容器的invalidateChild,逐級回溯,最終走到ViewRootImpl的invalidate void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,             boolean fullInvalidate) {             // Propagate the damage rectangle to the parent view.             final AttachInfo ai = mAttachInfo;             final ViewParent p = mParent;             if (p != null && ai != null && l < r && t < b) {                 final Rect damage = ai.mTmpInvalRect;                 damage.set(l, t, r, b);                 p.invalidateChild(this, damage);             } ViewRootImpl.java void invalidate() {     mDirty.set(0, 0, mWidth, mHeight);     if (!mWillDrawSoon) {         scheduleTraversals();     } } ViewRootImpl會呼叫scheduleTraversals準備重繪,但是,重繪一般不會立即執行,而是往Choreographer的Choreographer.CALLBACK_TRAVERSAL佇列中添加了一個mTraversalRunnable,同時申請VSYNC,這個mTraversalRunnable要一直等到申請的VSYNC到來後才會被執行;

3、scheduleTraversals ``` ViewRootImpl.java

// 將UI繪製的mTraversalRunnable加入到下次垂直同步訊號到來的等待callback中去

// mTraversalScheduled用來保證本次Traversals未執行前,不會要求遍歷兩邊,浪費16ms內,不需要繪製兩次

void scheduleTraversals() {

if (!mTraversalScheduled) {

mTraversalScheduled = true;

// 防止同步柵欄,同步柵欄的意思就是攔截同步訊息

mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

// postCallback的時候,順便請求vnsc垂直同步訊號scheduleVsyncLocked

mChoreographer.postCallback(

Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

if (!mUnbufferedInputDispatch) {

scheduleConsumeBatchedInput();

}

notifyRendererOfFramePending();

pokeDrawLockIfNeeded();

}

} ``` 4、申請VSYNC同步訊號

Choreographer知識點在上個文章詳細介紹過; ``` Choreographer.java

private void postCallbackDelayedInternal(int callbackType,

Object action, Object token, long delayMillis) {

synchronized (mLock) {

final long now = SystemClock.uptimeMillis();

final long dueTime = now + delayMillis;

mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

if (dueTime <= now) {

scheduleFrameLocked(now);

}

} **5、scheduleFrameLocked** // mFrameScheduled保證16ms內,只會申請一次垂直同步訊號

// scheduleFrameLocked可以被呼叫多次,但是mFrameScheduled保證下一個vsync到來之前,不會有新的請求發出

// 多餘的scheduleFrameLocked呼叫被無效化

private void scheduleFrameLocked(long now) {

if (!mFrameScheduled) {

mFrameScheduled = true;

if (USE_VSYNC) {

if (isRunningOnLooperThreadLocked()) {

scheduleVsyncLocked();

} else {

// 因為invalid已經有了同步柵欄,所以必須mFrameScheduled,訊息才能被UI執行緒執行

Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);

msg.setAsynchronous(true);

mHandler.sendMessageAtFrontOfQueue(msg);

}

}  

}

} ``` - 在當前申請的VSYNC到來之前,不會再去請求新的VSYNC,因為16ms內申請兩個VSYNC沒意義; - 再VSYNC到來之後,Choreographer利用Handler將FrameDisplayEventReceiver封裝成一個非同步Message,傳送到UI執行緒的MessageQueue;

6、FrameDisplayEventReceiver ```   private final class FrameDisplayEventReceiver extends DisplayEventReceiver

implements Runnable {

private boolean mHavePendingVsync;

private long mTimestampNanos;

private int mFrame;

public FrameDisplayEventReceiver(Looper looper) {

super(looper);

}

@Override

public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {

long now = System.nanoTime();

if (timestampNanos > now) {

timestampNanos = now;

}

if (mHavePendingVsync) {

Log.w(TAG, "Already have a pending vsync event.  There should only be "

+ "one at a time.");

} 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);

}

} ``` - 之所以封裝成非同步Message,是因為前面添加了一個同步柵欄,同步訊息不會被執行; - UI執行緒被喚起,取出該訊息,最終呼叫doFrame進行UI重新整理重繪;

7、doFrame ``` void doFrame(long frameTimeNanos, int frame) {

final long startNanos;

synchronized (mLock) {

if (!mFrameScheduled) {

return; // no work to do

}

long intendedFrameTimeNanos = frameTimeNanos;

startNanos = System.nanoTime();

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.");

}

final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;

frameTimeNanos = startNanos - lastFrameOffset;

}

if (frameTimeNanos < mLastFrameTimeNanos) {

scheduleVsyncLocked();

return;

}

mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);

mFrameScheduled = false;

mLastFrameTimeNanos = frameTimeNanos;

}

try {

Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");

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 {

Trace.traceEnd(Trace.TRACE_TAG_VIEW);

}

} - doFrame也採用了一個boolean遍歷mFrameScheduled保證每次VSYNC中,只執行一次,可以看到,為了保證16ms只執行一次重繪,加了好多次層保障; - doFrame裡除了UI重繪,其實還處理了很多其他的事,比如檢測VSYNC被延遲多久執行,掉了多少幀,處理Touch事件(一般是MOVE),處理動畫,以及UI; - 當doFrame在處理Choreographer.CALLBACK_TRAVERSAL的回撥時(mTraversalRunnable),才是真正的開始處理View重繪;   final class TraversalRunnable implements Runnable {

@Override

public void run() {

doTraversal();

}

} ``` 回到ViewRootImpl呼叫doTraversal進行View樹遍歷;

8、doTraversal ``` // 這裡是真正執行了,

void doTraversal() {

if (mTraversalScheduled) {

mTraversalScheduled = false;

mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

performTraversals();

}

} ``` - doTraversal會先將柵欄移除,然後處理performTraversals,進行測量、佈局、繪製,提交當前幀給SurfaceFlinger進行圖層合成顯示; - 以上多個boolean變數保證了每16ms最多執行一次UI重繪;

9、UI區域性重繪

View重繪重新整理,並不會導致所有View都進行一次measure、layout、draw,只是這個待重新整理View鏈路需要調整,剩餘的View可能不需要浪費精力再來一遍; ``` View.java

public RenderNode updateDisplayListIfDirty() {

final RenderNode renderNode = mRenderNode;

...

if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0

|| !renderNode.isValid()

|| (mRecreateDisplayList)) {

} else {

mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;

mPrivateFlags &= ~PFLAG_DIRTY_MASK;

}

return renderNode;

} ``` 10、繪製總結

  • android最高60FPS,是VSYNC及決定的,每16ms最多一幀;
  • VSYNC要客戶端主動申請,才會有;
  • 有VSYNC到來才會重新整理;
  • UI沒更改,不會請求VSYNC也就不會重新整理;

總結

關於繪製還有很多知識點,後面會總結陸續發出來的;