【Android】從Choreographer到UI渲染(二)
Google 在 2012 年推出的 Project Butter(黃油計劃)是 Android 系統發展史上的重要里程碑,旨在解決長期存在的 UI 卡頓、響應延遲等問題,提升用戶體驗。
在 Android 4.1 之前,系統缺乏統一的幀同步機制,屏幕刷新、應用渲染與用戶輸入之間常因時序錯亂導致畫面撕裂(Screen Tearing)或幀丟失(Jank)。
例如,雙緩沖機制下,若 GPU 渲染超時,CPU 無法及時處理下一幀,導致后續幀被迫跳過多個刷新周期,用戶會明顯感知卡頓;觸摸事件的處理也因未與屏幕刷新同步,出現“不跟手”的延遲。這些問題嚴重影響了用戶體驗,尤其在動畫和滾動場景中尤為突出。
為此,Google 通過 VSync 垂直同步、三重緩沖(Triple Buffering) 和 Choreographer 調度框架 三管齊下重構顯示系統:VSync 信號將 CPU/GPU 的渲染周期與屏幕刷新嚴格對齊,確保每幀的準備工作在 16ms(60Hz 屏幕)內啟動;三重緩沖通過增加臨時緩沖區緩解 GPU 超時導致的連續丟幀問題,犧牲少量內存換取流暢性;而 Choreographer 作為“舞蹈編導”,統一調度輸入、動畫、繪制等任務,在 VSync 信號到達時批量執行,避免任務碎片化。
垂直同步和三緩沖已經在之前的篇章中提到過,所以今天我們詳細分析一下Choregrapher。
本文參考:
Android 之 Choreographer 詳細分析 - 簡書
何時調用Choreographer
在 Android 的 UI 渲染機制中,無論是 Activity 啟動后的首次布局,還是后續通過動畫或手動觸發的界面更新,最終都會匯聚到 ViewRootImpl 的 scheduleTraversals()
方法。
我們首先來回憶一下Activity的啟動與Window的添加過程。
當 Activity 完成 onResume()
后,系統會將其視圖層級(DecorView)添加到 Window 中。具體步驟如下:
- Window 創建:Activity 的
Window
(通常是PhoneWindow
)在onCreate()
階段初始化,并在onResume()
后通過WindowManager
添加到系統。- ViewRootImpl 關聯:
WindowManagerGlobal.addView()
方法會創建 ViewRootImpl 實例,并調用其setView()
方法,將 DecorView 與 ViewRootImpl 綁定。- 觸發首次布局:在
ViewRootImpl.setView()
中,調用requestLayout()
發起首次測量、布局、繪制請求。
// ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {// 關聯 DecorViewmView = view;// 請求布局requestLayout();// ... 其他初始化邏輯
}
requestLayout()
是 UI 更新的起點,其核心邏輯如下:
- 標記布局請求:通過
mLayoutRequested
標志位,確保同一幀內多次調用只觸發一次布局。- 調度遍歷任務:調用
scheduleTraversals()
,安排一次performTraversals()
的執行。
// ViewRootImpl.java
@Override
public void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {checkThread();mLayoutRequested = true;scheduleTraversals();}
}
scheduleTraversals()
負責協調 UI 更新與 VSync 信號的同步:
- 同步屏障(Sync Barrier):通過
postSyncBarrier()
向主線程的 MessageQueue 插入一個同步屏障,屏蔽普通同步消息,確保 UI 更新任務優先執行。 - 提交回調到 Choreographer:將
mTraversalRunnable
(最終觸發doTraversal()
)提交給 Choreographer,在下一個 VSync 信號到達時執行。
//ViewRootImpl.javavoid scheduleTraversals() {if (!mTraversalScheduled) {//此字段保證同時間多次更改只會刷新一次,例如TextView連續兩次setText(),也只會走一次繪制流程mTraversalScheduled = true;//添加同步屏障,屏蔽同步消息,保證VSync到來立即執行繪制mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//mTraversalRunnable是TraversalRunnable實例,最終走到run(),也即doTraversal();mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);if (!mUnbufferedInputDispatch) {scheduleConsumeBatchedInput();}notifyRendererOfFramePending();pokeDrawLockIfNeeded();}}final class TraversalRunnable implements Runnable {@Overridepublic void run() {doTraversal();}}final TraversalRunnable mTraversalRunnable = new TraversalRunnable();void doTraversal() {if (mTraversalScheduled) {mTraversalScheduled = false;//移除同步屏障mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);...//開始三大繪制流程performTraversals();...}}
Choreographer的創建
它的實例mChoreographer,是在ViewRootImpl的構造方法內使用Choreographer.getInstance()創建:
Choreographer mChoreographer;//ViewRootImpl實例是在添加window時創建
public ViewRootImpl(Context context, Display display) {...mChoreographer = Choreographer.getInstance();...
}
Choreographer
通過 ThreadLocal 實現線程單例,確保每個線程(尤其是主線程)擁有獨立的實例。這種設計類似于 Looper
的線程綁定機制,原因如下:
- 線程隔離性:UI 操作必須在主線程執行,
Choreographer
需要與主線程的Looper
綁定,以確保任務調度與消息循環同步。 - 避免競爭:多線程環境下,獨立實例可防止任務隊列的并發沖突。
// 通過 ThreadLocal 存儲每個線程的 Choreographer 實例
private static final ThreadLocal<Choreographer> sThreadInstance = new ThreadLocal<Choreographer>() {@Overrideprotected Choreographer initialValue() {// 必須綁定到帶有 Looper 的線程Looper looper = Looper.myLooper();if (looper == null) {throw new IllegalStateException("當前線程必須擁有 Looper!");}// 創建實例并與 Looper 關聯Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);// 如果是主線程,記錄為主實例if (looper == Looper.getMainLooper()) {mMainInstance = choreographer;}return choreographer;}};public static Choreographer getInstance() {return sThreadInstance.get(); // 返回當前線程的實例
}
關鍵點:
- 強制 Looper 存在:若線程沒有
Looper
(如未調用Looper.prepare()
),直接拋出異常。這解釋了為什么 UI 操作必須在主線程(主線程默認初始化了Looper
)。 - 主線程標識:通過
Looper.getMainLooper()
判斷當前線程是否為主線程,并記錄主實例供全局訪問。
緊接著是Chroreographer的構造方法:
private Choreographer(Looper looper, int vsyncSource) {mLooper = looper;//使用當前線程looper創建 mHandlermHandler = new FrameHandler(looper);//USE_VSYNC 4.1以上默認是true,表示 具備接受VSync的能力,這個接受能力就是FrameDisplayEventReceivermDisplayEventReceiver = USE_VSYNC? new FrameDisplayEventReceiver(looper, vsyncSource): null;mLastFrameTimeNanos = Long.MIN_VALUE;// 計算一幀的時間,Android手機屏幕是60Hz的刷新頻率,就是16msmFrameIntervalNanos = (long)(1000000000 / getRefreshRate());// 創建一個鏈表類型CallbackQueue的數組,大小為5,//也就是數組中有五個鏈表,每個鏈表存相同類型的任務:輸入、動畫、遍歷繪制等任務(CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL)mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];for (int i = 0; i <= CALLBACK_LAST; i++) {mCallbackQueues[i] = new CallbackQueue();}// b/68769804: For low FPS experiments.setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));}
Choreographer
的構造函數初始化了多個關鍵組件,這些組件共同協作完成幀調度:
FrameHandler:異步消息處理器
private final class FrameHandler extends Handler {public FrameHandler(Looper looper) {super(looper); // 綁定當前線程的 Looper}@Overridepublic void handleMessage(Message msg) {switch (msg.what) {case MSG_DO_FRAME: // 執行幀任務doFrame(System.nanoTime(), 0);break;case MSG_DO_SCHEDULE_VSYNC: // 請求 VSync 信號doScheduleVsync();break;case MSG_DO_SCHEDULE_CALLBACK: // 延遲任務調度doScheduleCallback(msg.arg1);break;}}
}
- 作用:處理與幀調度相關的異步消息(如
MSG_DO_FRAME
),確保任務在主線程執行。 - 異步消息:通過
msg.setAsynchronous(true)
標記消息為異步,繞過同步屏障(后文詳述)。
FrameDisplayEventReceiver:VSync 信號接收器
// USE_VSYNC 默認為 true(Android 4.1+ 啟用)
mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper, vsyncSource) : null;
- 功能:通過 JNI 層注冊到顯示系統,監聽硬件 VSync 信號。
- 信號回調:當 VSync 信號到達時,觸發
onVsync()
方法,進而通過Choreographer
調度幀任務。
mFrameIntervalNanos:幀間隔時間
// 60Hz 屏幕:1秒 / 60幀 ≈ 16.666ms
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
- 計算依據:基于屏幕刷新率(如 60Hz)計算每幀的理想時間(16.6ms)。
- 用途:在
doFrame()
中檢測幀超時(如判斷是否跳幀)。
mCallbackQueues:任務隊列數組
// 五種任務類型:輸入、動畫、插入動畫、遍歷繪制、提交
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {mCallbackQueues[i] = new CallbackQueue();
}
-
任務分類:將回調任務按類型存入不同隊列,確保執行順序:
CALLBACK_INPUT:處理觸摸/輸入事件(優先級最高)。
CALLBACK_ANIMATION:執行屬性動畫、過渡動畫。
CALLBACK_INSETS_ANIMATION:窗口插入動畫(如狀態欄/導航欄變化)。
CALLBACK_TRAVERSAL:執行
View
的測量、布局、繪制。CALLBACK_COMMIT:提交幀數據到渲染線程(最后執行)。
Choreographer調用流程
根據不同的任務類型分派任務
當調用 mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, ...)
提交任務時,系統根據任務類型(如 CALLBACK_TRAVERSAL
表示繪制任務)將任務存入對應的 CallbackQueue
隊列。
postCallback()內部調用postCallbackDelayed(),接著又調用postCallbackDelayedInternal()
private void postCallbackDelayedInternal(int callbackType, Object action, long delayMillis) {synchronized (mLock) {// 計算任務的到期時間final long dueTime = SystemClock.uptimeMillis() + delayMillis;// 將任務添加到對應類型的隊列mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, null);if (dueTime <= now) {scheduleFrameLocked(now); // 立即調度} else {// 發送延遲消息,最終仍觸發 scheduleFrameLocked()Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);msg.arg1 = callbackType;msg.setAsynchronous(true); // 關鍵:異步消息mHandler.sendMessageAtTime(msg, dueTime);}}
}
postCallbackDelayedInternal()
方法會根據延遲時間決定立即調度或延遲處理:若任務需要立即執行,直接調用 scheduleFrameLocked()
申請 VSync 信號;若存在延遲,則通過 FrameHandler
發送異步消息 MSG_DO_SCHEDULE_CALLBACK
,最終仍會觸發 scheduleFrameLocked()
。異步消息的標記(msg.setAsynchronous(true)
)是關鍵設計,它允許這些消息繞過同步屏障——在 ViewRootImpl.scheduleTraversals()
中插入的同步屏障會阻塞普通同步消息,確保 UI 渲染任務優先執行。
private final class FrameHandler extends Handler {public FrameHandler(Looper looper) {super(looper);}@Overridepublic void handleMessage(Message msg) {switch (msg.what) {case MSG_DO_FRAME:// 執行doFrame,即繪制過程doFrame(System.nanoTime(), 0);break;case MSG_DO_SCHEDULE_VSYNC://申請VSYNC信號,例如當前需要繪制任務時doScheduleVsync();break;case MSG_DO_SCHEDULE_CALLBACK://需要延遲的任務,最終還是執行上述兩個事件doScheduleCallback(msg.arg1);break;}}}
進第三個case試試。
void doScheduleCallback(int callbackType) {synchronized (mLock) {if (!mFrameScheduled) {final long now = SystemClock.uptimeMillis();if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {scheduleFrameLocked(now);}}}}
發現也是走到這里,即延遲運行最終也會走到scheduleFrameLocked()
private void scheduleFrameLocked(long now) {if (!mFrameScheduled) {mFrameScheduled = true;//開啟了VSYNCif (USE_VSYNC) {if (DEBUG_FRAMES) {Log.d(TAG, "Scheduling next frame on vsync.");}//當前執行的線程,是否是mLooper所在線程if (isRunningOnLooperThreadLocked()) {//申請 VSYNC 信號scheduleVsyncLocked();} else {// 若不在,就用mHandler發送消息到原線程,最后還是調用scheduleVsyncLocked方法Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);msg.setAsynchronous(true);//異步mHandler.sendMessageAtFrontOfQueue(msg);}} else {// 如果未開啟VSYNC則直接doFrame方法(4.1后默認開啟)final long nextFrameTime = Math.max(mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);if (DEBUG_FRAMES) {Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");}Message msg = mHandler.obtainMessage(MSG_DO_FRAME);msg.setAsynchronous(true);//異步mHandler.sendMessageAtTime(msg, nextFrameTime);}}}
FrameHandler的作用很明顯里了:發送異步消息(因為前面設置了同步屏障)。有延遲的任務發延遲消息、不在原線程的發到原線程、沒開啟VSYNC的直接走 doFrame 方法取執行繪制。
申請與接受VSync
Choreographer
會通過 scheduleVsyncLocked()
方法向系統申請 VSync 信號。
private void scheduleVsyncLocked() {mDisplayEventReceiver.scheduleVsync();}
這一過程的核心在于 FrameDisplayEventReceiver
,也就是此處的mDisplayEventReceiver,它是 DisplayEventReceiver
的子類,在構造時通過 JNI 層與底層顯示服務建立連接(nativeInit
),注冊為 VSync 信號的監聽者。當調用 scheduleVsync()
時,實際通過 nativeScheduleVsync
這一本地方法向系統請求下一次 VSync 中斷信號。一旦硬件產生 VSync 脈沖(例如屏幕準備刷新下一幀時),系統會回調 FrameDisplayEventReceiver
的 onVsync
方法,此時該方法將當前 VSync 的時間戳、顯示設備 ID 和幀序號記錄下來,并將自身封裝為一個異步消息(msg.setAsynchronous(true)
)發送到主線程的 MessageQueue
。
接下來看看代碼怎么說:
public DisplayEventReceiver(Looper looper, int vsyncSource) {if (looper == null) {throw new IllegalArgumentException("looper must not be null");}mMessageQueue = looper.getQueue();// 注冊VSYNC信號監聽者mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,vsyncSource);mCloseGuard.open("dispose");}
在 DisplayEventReceiver 的構造方法會通過 JNI 創建一個 IDisplayEventConnection 的 VSYNC 的監聽者。
FrameDisplayEventReceiver的scheduleVsync()就是在 DisplayEventReceiver中:
public void scheduleVsync() {if (mReceiverPtr == 0) {Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event " + "receiver has already been disposed.");} else {// 申請VSYNC中斷信號,會回調onVsync方法nativeScheduleVsync(mReceiverPtr);}}
當調用 scheduleVsync()
時,實際通過 nativeScheduleVsync
這一本地方法向系統請求下一次 VSync 中斷信號。
一旦硬件產生 VSync 脈沖(例如屏幕準備刷新下一幀時),系統會回調 FrameDisplayEventReceiver
的 onVsync
方法,此時該方法將當前 VSync 的時間戳、顯示設備 ID 和幀序號記錄下來,并將自身封裝為一個異步消息(msg.setAsynchronous(true)
)發送到主線程的 MessageQueue
。
/*** 接收到VSync脈沖時 回調* @param timestampNanos VSync脈沖的時間戳* @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.* @param frame 幀號碼,自增*/@UnsupportedAppUsagepublic void onVsync(long timestampNanos, long physicalDisplayId, int frame) {}
具體實現是在FrameDisplayEventReceiver中:
private final class FrameDisplayEventReceiver extends DisplayEventReceiverimplements Runnable {private boolean mHavePendingVsync;private long mTimestampNanos;private int mFrame;public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {super(looper, vsyncSource);}@Overridepublic void onVsync(long timestampNanos, long physicalDisplayId, int frame) {// Post the vsync event to the Handler.// The idea is to prevent incoming vsync events from completely starving// the message queue. If there are no messages in the queue with timestamps// earlier than the frame time, then the vsync event will be processed immediately.// Otherwise, messages that predate the vsync event will be handled first.long now = System.nanoTime();if (timestampNanos > now) {Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)+ " ms in the future! Check that graphics HAL is generating vsync "+ "timestamps using the correct timebase.");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;//將本身作為runnable傳入msg, 發消息后 會走run(),即doFrame(),也是異步消息Message msg = Message.obtain(mHandler, this);msg.setAsynchronous(true);mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);}@Overridepublic void run() {mHavePendingVsync = false;doFrame(mTimestampNanos, mFrame);}}
當主線程處理到這條異步消息時,會執行 FrameDisplayEventReceiver
的 run()
方法,進而調用 Choreographer.doFrame()
,這是整個渲染流程的起點。
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();// 超時時間是否超過一幀的時間(這是因為MessageQueue雖然添加了同步屏障,但是還是有正在執行的同步任務,導致doFrame延遲執行了)final long jitterNanos = startNanos - frameTimeNanos;if (jitterNanos >= mFrameIntervalNanos) {// 計算掉幀數final long skippedFrames = jitterNanos / mFrameIntervalNanos;if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {// 掉幀超過30幀打印Log提示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;}...mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);// Frame標志位恢復mFrameScheduled = false;// 記錄最后一幀時間mLastFrameTimeNanos = frameTimeNanos;}try {// 按類型順序 執行任務Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);mFrameInfo.markInputHandlingStart();doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);mFrameInfo.markAnimationsStart();doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);mFrameInfo.markPerformTraversalsStart();doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);} finally {AnimationUtils.unlockAnimationClock();Trace.traceEnd(Trace.TRACE_TAG_VIEW);}}
在 doFrame()
中,系統首先通過計算當前時間與 VSync 信號時間戳的差值(jitterNanos = startNanos - frameTimeNanos
)判斷是否發生跳幀:若差值超過一幀的理論時長(如 16.6ms 對應 60Hz 屏幕),則按超出時長除以單幀時間計算跳幀數,若超過 30 幀(約 500ms)則打印警告日志,提示主線程可能存在耗時操作。
隨后,系統按固定順序處理任務隊列——依次執行輸入事件(CALLBACK_INPUT
)、動畫更新(CALLBACK_ANIMATION
)、窗口插入動畫(CALLBACK_INSETS_ANIMATION
)、視圖樹遍歷(CALLBACK_TRAVERSAL
)和幀提交(CALLBACK_COMMIT
)。
每個隊列的任務通過 doCallbacks()
方法提取并執行:例如 CALLBACK_TRAVERSAL
隊列中的任務通常是 ViewRootImpl
提交的 mTraversalRunnable
,其 run()
方法最終觸發 performTraversals()
,執行測量、布局、繪制三大流程。
void doCallbacks(int callbackType, long frameTimeNanos) {CallbackRecord callbacks;synchronized (mLock) {final long now = System.nanoTime();// 根據指定的類型CallbackkQueue中查找到達執行時間的CallbackRecordcallbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);if (callbacks == null) {return;}mCallbacksRunning = true;//提交任務類型if (callbackType == Choreographer.CALLBACK_COMMIT) {final long jitterNanos = now - frameTimeNanos;if (jitterNanos >= 2 * mFrameIntervalNanos) {final long lastFrameOffset = jitterNanos % mFrameIntervalNanos+ mFrameIntervalNanos;if (DEBUG_JANK) {Log.d(TAG, "Commit callback delayed by " + (jitterNanos * 0.000001f)+ " ms which is more than twice the frame interval of "+ (mFrameIntervalNanos * 0.000001f) + " ms! "+ "Setting frame time to " + (lastFrameOffset * 0.000001f)+ " ms in the past.");mDebugPrintNextFrameTimeDelta = true;}frameTimeNanos = now - lastFrameOffset;mLastFrameTimeNanos = frameTimeNanos;}}}try {// 迭代執行隊列所有任務for (CallbackRecord c = callbacks; c != null; c = c.next) {// 回調CallbackRecord的run,其內部回調Callback的runc.run(frameTimeNanos);}} finally {synchronized (mLock) {mCallbacksRunning = false;do {final CallbackRecord next = callbacks.next;//回收CallbackRecordrecycleCallbackLocked(callbacks);callbacks = next;} while (callbacks != null);}}}
任務的具體執行由 CallbackRecord.run()
完成,該函數根據 token
判斷任務類型——若 token
為 FRAME_CALLBACK_TOKEN
(通過 postFrameCallback()
提交),則調用 FrameCallback.doFrame()
接口;否則直接執行 Runnable.run()
。
private static final class CallbackRecord {public CallbackRecord next;public long dueTime;public Object action; // Runnable or FrameCallbackpublic Object token;@UnsupportedAppUsagepublic void run(long frameTimeNanos) {if (token == FRAME_CALLBACK_TOKEN) {// 通過postFrameCallback 或 postFrameCallbackDelayed,會執行這里((FrameCallback)action).doFrame(frameTimeNanos);} else {//取出Runnable執行run()((Runnable)action).run();}}}
前面看到mChoreographer.postCallback傳的token是null,所以取出action,就是Runnable,執行run(),這里的action就是 ViewRootImpl 發起的繪制任務mTraversalRunnable了,那么這樣整個邏輯就閉環了。
那么 啥時候 token == FRAME_CALLBACK_TOKEN 呢?答案是Choreographer的postFrameCallback()方法:
public void postFrameCallback(FrameCallback callback) {postFrameCallbackDelayed(callback, 0);}public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {if (callback == null) {throw new IllegalArgumentException("callback must not be null");}//也是走到是postCallbackDelayedInternal,并且注意是CALLBACK_ANIMATION類型,//token是FRAME_CALLBACK_TOKEN,action就是FrameCallbackpostCallbackDelayedInternal(CALLBACK_ANIMATION,callback, FRAME_CALLBACK_TOKEN, delayMillis);}public interface FrameCallback {public void doFrame(long frameTimeNanos);}
計算丟幀
舉個栗子:
//Application.javapublic void onCreate() {super.onCreate();//在Application中使用postFrameCallbackChoreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));}public class FPSFrameCallback implements Choreographer.FrameCallback {private static final String TAG = "FPS_TEST";private long mLastFrameTimeNanos = 0;private long mFrameIntervalNanos;public FPSFrameCallback(long lastFrameTimeNanos) {mLastFrameTimeNanos = lastFrameTimeNanos;mFrameIntervalNanos = (long)(1000000000 / 60.0);}@Overridepublic void doFrame(long frameTimeNanos) {//初始化時間if (mLastFrameTimeNanos == 0) {mLastFrameTimeNanos = frameTimeNanos;}final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;if (jitterNanos >= mFrameIntervalNanos) {final long skippedFrames = jitterNanos / mFrameIntervalNanos;if(skippedFrames>30){//丟幀30以上打印日志Log.i(TAG, "Skipped " + skippedFrames + " frames! "+ "The application may be doing too much work on its main thread.");}}mLastFrameTimeNanos=frameTimeNanos;//注冊下一幀回調Choreographer.getInstance().postFrameCallback(this);}}
通過 postFrameCallback()
注冊一個 FrameCallback
,在每次 doFrame()
時計算相鄰兩次 VSync 信號的時間差,若超過閾值則判定為跳幀。例如,示例中的 FPSFrameCallback
在每次回調時比較當前幀與上一幀的時間差,若超過 16.6ms 的倍數,則累加跳幀數并打印警告。
這種設計使得所有 UI 操作(無論是觸摸、動畫還是繪制)都被嚴格對齊到 VSync 信號,既避免了畫面撕裂,又為每一幀提供了完整的處理窗口,而開發者可通過監聽回調精準定位主線程卡頓的根源,例如在 doFrame
中插入性能埋點或結合 Systrace
工具分析耗時任務。