Android VSYNC (Choreographer)與UI刷新原理分析

從UI控件內容更改到被從新繪製到屏幕上,這中間到底經歷了什麼?另外,連續兩次setTextView到底會觸發幾回UI重繪呢?爲何Android APP的幀率最高是60FPS呢,這就是本文要討論的內容。java

以電影爲例,動畫至少要達到24FPS,才能保證畫面的流暢性,低於這個值,肉眼會感受到卡頓。在手機上,這個值被調整到60FPS,增長絲滑度,這也是爲何有個(1000/60)16ms的指標,通常而言目前的Android系統最高FPS也就是60,它是經過了一個VSYNC來保證每16ms最多繪製一幀。簡而言之:UI必須至少等待16ms的間隔纔會繪製下一幀,因此連續兩次setTextView只會觸發一次重繪。下面來具體看一下UI的重繪流程。android

UI刷新流程示意

以Textview爲例 ,當咱們經過setText改變TextView內容後,UI界面不會馬上改變,APP端會先向VSYNC服務請求,等到下一次VSYNC信號觸發後,APP端的UI才真的開始刷新,基本流程以下app

image.png

從咱們的代碼端來看以下:setText最終調用invalidate申請重繪,最後會經過ViewParent遞歸到ViewRootImpl的invalidate,請求VSYNC,在請求VSYNC的時候,會添加一個同步柵欄,防止UI線程中同步消息執行,這樣作爲了加快VSYNC的響應速度,若是不設置,VSYNC到來的時候,正在執行一個同步消息,那麼UI更新的Task就會被延遲執行,這是Android的Looper跟MessageQueue決定的。異步

APP端觸發重繪,申請VSYNC流程示意ide

image.png

等到VSYNC到來後,會移除同步柵欄,並率先開始執行當前幀的處理,調用邏輯以下工具

VSYNC回來流程示意oop

image.png

doFrame執行UI繪製的示意圖佈局

image.png

UI刷新源碼跟蹤

同TextView相似,View內容改變通常都會調用invalidate觸發視圖重繪,這中間經歷了什麼呢?View會遞歸的調用父容器的invalidateChild,逐級回溯,最終走到ViewRootImpl的invalidate,以下:post

View.java動畫

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到來後纔會被執行,以下:

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);
         <!--添加一個處理觸摸事件的回調,防止中間有Touch事件過來-->
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
複製代碼

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) {
        <!--申請VSYNC同步信號-->
            scheduleFrameLocked(now);
        } 
    }
}
複製代碼

scheduleTraversals利用mTraversalScheduled保證,在當前的mTraversalRunnable未被執行前,scheduleTraversals不會再被有效調用,也就是Choreographer.CALLBACK_TRAVERSAL理論上應該只有一個mTraversalRunnable的Task。mChoreographer.postCallback將mTraversalRunnable插入到CallBack以後,會接着調用scheduleFrameLocked請求Vsync同步信號

// 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);
            }
        }  
    }
}
複製代碼

scheduleFrameLocked跟上一個scheduleTraversals相似,也採用了利用mFrameScheduled來保證:在當前申請的VSYNC到來以前,不會再去請求新的VSYNC,由於16ms內申請兩個VSYNC沒意義。再VSYNC到來以後,Choreographer利用Handler將FrameDisplayEventReceiver封裝成一個異步Message,發送到UI線程的MessageQueue,

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,通常是上傳vsync的機制出了問題-->
                timestampNanos = now;
            }
            <!--若是上一個vsync同步信號沒執行,那就不該該相應下一個(多是其餘線程經過某種方式請求的)-->
	          if (mHavePendingVsync) {
                Log.w(TAG, "Already have a pending vsync event.  There should only be "
                        + "one at a time.");
            } else {
                mHavePendingVsync = true;
            }
            <!--timestampNanos實際上是本次vsync產生的時間,從服務端發過來-->
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            <!--因爲已經存在同步柵欄,因此VSYNC到來的Message須要做爲異步消息發送過去-->
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            mHavePendingVsync = false;
            <!--這裏的mTimestampNanos其實就是本次Vynsc同步信號到來的時候,可是執行這個消息的時候,可能延遲了-->
            doFrame(mTimestampNanos, mFrame);
        }
    }
複製代碼

之因此封裝成異步Message,是由於前面添加了一個同步柵欄,同步消息不會被執行。UI線程被喚起,取出該消息,最終調用doFrame進行UI刷新重繪

void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
    <!--作了不少東西,都是爲了保證一次16ms有一次垂直同步信號,有一次input 、刷新、重繪-->
        if (!mFrameScheduled) {
            return; // no work to do
        }
       long intendedFrameTimeNanos = frameTimeNanos;
        startNanos = System.nanoTime();
        final long jitterNanos = startNanos - frameTimeNanos;
        <!--檢查是否由於延遲執行掉幀,每大於16ms,就多掉一幀-->
        if (jitterNanos >= mFrameIntervalNanos) {
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            <!--跳幀,其實就是上一次請求刷新被延遲的時間,可是這裏skippedFrames爲0不表明沒有掉幀-->
            if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
            <!--skippedFrames很大必定掉幀,可是爲 0,去並不是沒掉幀-->
                Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                        + "The application may be doing too much work on its main thread.");
            }
            final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
				<!--開始doFrame的真正有效時間戳-->
            frameTimeNanos = startNanos - lastFrameOffset;
        }

        if (frameTimeNanos < mLastFrameTimeNanos) {
            <!--這種狀況通常是生成vsync的機制出現了問題,那就再申請一次-->
            scheduleVsyncLocked();
            return;
        }
		  <!--intendedFrameTimeNanos是原本要繪製的時間戳,frameTimeNanos是真正的,能夠在渲染工具中標識延遲VSYNC多少-->
        mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
        <!--移除mFrameScheduled判斷,說明處理開始了,-->
        mFrameScheduled = false;
        <!--更新mLastFrameTimeNanos-->
        mLastFrameTimeNanos = frameTimeNanos;
    }

    try {
    	 <!--真正開始處理業務-->
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
		<!--處理打包的move事件-->
        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樹遍歷,

// 這裏是真正執行了,
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        <!--移除同步柵欄,只有重繪才設置了柵欄,說明重繪的優先級仍是挺高的,全部的同步消息必須讓步-->
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        performTraversals();
    }
}
複製代碼

doTraversal會先將柵欄移除,而後處理performTraversals,進行測量、佈局、繪製,提交當前幀給SurfaceFlinger進行圖層合成顯示。以上多個boolean變量保證了每16ms最多執行一次UI重繪,這也是目前Android存在60FPS上限的緣由。

注: VSYNC同步信號須要用戶主動去請求才會收到,而且是單次有效。

UI局部重繪

某一個View重繪刷新,並不會致使全部View都進行一次measure、layout、draw,只是這個待刷新View鏈路須要調整,剩餘的View可能不須要浪費精力再來一遍,反應再APP側就是:不須要再次調用全部ViewupdateDisplayListIfDirty構建RenderNode渲染Op樹,以下

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;
    }
複製代碼

總結

  • android最高60FPS,是VSYNC及決定的,每16ms最多一幀
  • VSYNC要客戶端主動申請,纔會有
  • 有VSYNC到來纔會刷新
  • UI沒更改,不會請求VSYNC也就不會刷新
  • UI局部重繪其實只是省去了再次構建硬件加速用的DrawOp樹(複用上衣幀的)

做者:看書的小蝸牛

Android VSYNC (Choreographer)與UI刷新原理分析.md

僅供參考,歡迎指正

相關文章
相關標籤/搜索