從UI控件內容更改到被從新繪製到屏幕上,這中間到底經歷了什麼?另外,連續兩次setTextView到底會觸發幾回UI重繪呢?爲何Android APP的幀率最高是60FPS呢,這就是本文要討論的內容。java
以電影爲例,動畫至少要達到24FPS,才能保證畫面的流暢性,低於這個值,肉眼會感受到卡頓。在手機上,這個值被調整到60FPS,增長絲滑度,這也是爲何有個(1000/60)16ms的指標,通常而言目前的Android系統最高FPS也就是60,它是經過了一個VSYNC來保證每16ms最多繪製一幀。簡而言之:UI必須至少等待16ms的間隔纔會繪製下一幀,因此連續兩次setTextView只會觸發一次重繪。下面來具體看一下UI的重繪流程。android
以Textview爲例 ,當咱們經過setText改變TextView內容後,UI界面不會馬上改變,APP端會先向VSYNC服務請求,等到下一次VSYNC信號觸發後,APP端的UI才真的開始刷新,基本流程以下app
從咱們的代碼端來看以下:setText最終調用invalidate申請重繪,最後會經過ViewParent遞歸到ViewRootImpl的invalidate,請求VSYNC,在請求VSYNC的時候,會添加一個同步柵欄,防止UI線程中同步消息執行,這樣作爲了加快VSYNC的響應速度,若是不設置,VSYNC到來的時候,正在執行一個同步消息,那麼UI更新的Task就會被延遲執行,這是Android的Looper跟MessageQueue決定的。異步
APP端觸發重繪,申請VSYNC流程示意ide
等到VSYNC到來後,會移除同步柵欄,並率先開始執行當前幀的處理,調用邏輯以下工具
VSYNC回來流程示意oop
doFrame執行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同步信號須要用戶主動去請求才會收到,而且是單次有效。
某一個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 VSYNC (Choreographer)與UI刷新原理分析.md
僅供參考,歡迎指正