【Android】從Choreographer到UI渲染(二)

【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 啟動后的首次布局,還是后續通過動畫或手動觸發的界面更新,最終都會匯聚到 ViewRootImplscheduleTraversals() 方法。

我們首先來回憶一下Activity的啟動與Window的添加過程。

當 Activity 完成 onResume() 后,系統會將其視圖層級(DecorView)添加到 Window 中。具體步驟如下:

  1. Window 創建:Activity 的 Window(通常是 PhoneWindow)在 onCreate() 階段初始化,并在 onResume() 后通過 WindowManager 添加到系統。
  2. ViewRootImpl 關聯WindowManagerGlobal.addView() 方法會創建 ViewRootImpl 實例,并調用其 setView() 方法,將 DecorView 與 ViewRootImpl 綁定。
  3. 觸發首次布局:在 ViewRootImpl.setView() 中,調用 requestLayout() 發起首次測量、布局、繪制請求。
// ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {// 關聯 DecorViewmView = view;// 請求布局requestLayout();// ... 其他初始化邏輯
}

requestLayout() 是 UI 更新的起點,其核心邏輯如下:

  1. 標記布局請求:通過 mLayoutRequested 標志位,確保同一幀內多次調用只觸發一次布局。
  2. 調度遍歷任務:調用 scheduleTraversals(),安排一次 performTraversals() 的執行。
// ViewRootImpl.java
@Override
public void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {checkThread();mLayoutRequested = true;scheduleTraversals();}
}

scheduleTraversals() 負責協調 UI 更新與 VSync 信號的同步:

  1. 同步屏障(Sync Barrier):通過 postSyncBarrier() 向主線程的 MessageQueue 插入一個同步屏障,屏蔽普通同步消息,確保 UI 更新任務優先執行。
  2. 提交回調到 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 脈沖(例如屏幕準備刷新下一幀時),系統會回調 FrameDisplayEventReceiveronVsync 方法,此時該方法將當前 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 脈沖(例如屏幕準備刷新下一幀時),系統會回調 FrameDisplayEventReceiveronVsync 方法,此時該方法將當前 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);}}

當主線程處理到這條異步消息時,會執行 FrameDisplayEventReceiverrun() 方法,進而調用 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 判斷任務類型——若 tokenFRAME_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 工具分析耗時任務。

放一個流程圖~

Android 之 Choreographer 詳細分析.png

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/80443.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/80443.shtml
英文地址,請注明出處:http://en.pswp.cn/web/80443.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

mvc-ioc實現

IOC 1&#xff09;耦合/依賴 依賴&#xff0c;是誰離不開誰 就比如上訴的Controller層必須依賴于Service層&#xff0c;Service層依賴于Dao 在軟件系統中&#xff0c;層與層之間存在依賴。我們稱之為耦合 我們系統架構或者設計的一個原則是&#xff…

MATLAB安裝常見問題解決方案

目前新版本的matlab安裝往往需要十幾G的本地安裝容量&#xff0c;例如matlab2022b、matlab2023b, 首先就是要保證本地硬盤空間足夠大&#xff0c;如果沒有足夠的本地內存空間&#xff0c;那么可以嘗試釋放本地硬盤空間&#xff0c;或者安裝所需內存空間較小的舊版本的matlab&am…

程序代碼篇---python獲取http界面上按鈕或者數據輸入

文章目錄 前言 前言 本文簡單接受了python獲取http界面上按鈕或者數據輸入

深入理解 Cortex-M3 特殊寄存器

在上一篇文章中分享了 Cortex-M3 內核寄存器組的相關知識&#xff0c;實際上除了內核寄存器組外&#xff0c;CM3 處理器中還存在多個特殊寄存器&#xff0c;它們分別為 程序狀態寄存器&#xff0c;中斷/異常屏蔽寄存器 和 控制寄存器。 需要注意的是&#xff0c;特殊寄存器未經…

標準庫、HAl庫和LL庫(PC13初始化)

標準庫 (Standard Peripheral Library) c #include "stm32f10x.h"void GPIO_Init_PC13(void) {GPIO_InitTypeDef GPIO_InitStruct;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);GPIO_InitStruct.GPIO_Pin GPIO_Pin_13;GPIO_InitStruct.GPIO_Mode GPIO_…

基于開源鏈動2+1模式AI智能名片S2B2C商城小程序的低集中度市場運營策略研究

摘要&#xff1a;本文聚焦于行業市場集中度問題&#xff0c;探討在低集中度市場中&#xff0c;如何利用開源鏈動21模式AI智能名片S2B2C商城小程序開展有效運營。分析了高集中度市場的競爭劣勢&#xff0c;闡述了開源鏈動21模式、AI智能名片以及S2B2C商城小程序的功能特點及其在…

一文讀懂-嵌入式Ubuntu平臺

現在直接在一些嵌入式Soc上移植ubuntu來用到產品上&#xff0c;剛開始感覺還挺臃腫的&#xff0c;后來細聊了下感覺還是有一定的優勢。 ubuntu相信大家在熟悉不過了&#xff0c;幾乎無處不在&#xff0c;小到咖啡機&#xff0c;大到火星車&#xff0c;為什么ubuntu如此廣泛&am…

箭頭函數及其與普通函數區別的詳細解釋

一、箭頭函數的基本特性 語法簡潔性 箭頭函數使用 > 符號定義&#xff0c;省略 function 關鍵字&#xff0c;適合快速定義匿名函數或簡單表達式。 // 普通函數 function sum(a, b) { return a b; } // 箭頭函數 const sum (a, b) > a b;若函數體為單行表達式&#x…

el-scrollbar 獲取滾動條高度 并將滾動條保持在低端

首先我們用ref綁定一個 scrollbar <el-scrollbar style"height: 100%;" ref"chatScrollRef" scroll"scrollTest">用scroll觸發滾動事件&#xff0c;一路滾到最底下&#xff0c;觀察三個屬性 const scrollTest ({scrollTop}) > {conso…

MyBatis-Plus 的 updateById 方法不更新 null 值屬性的問題

項目場景&#xff1a; 使用Mybatis-plus的updateById去更新實體類的時候&#xff0c;如果設置實體類中的某個字段為null&#xff0c;會導致為null的字段不做更新操作 問題描述 updateById方法 不會更新null值 解決方案&#xff1a; 在字段上加上 TableField(updateStrategy …

STC89C52單片機模擬實現洗衣機控制 Proteus仿真

用直流電機轉動模擬洗衣機。要求有弱洗、普通洗、強洗三種模式,可通過按鍵選擇相應模式。要求能夠設置洗衣時長,可以通過按鍵選擇15、30、45、60、90分鐘。定時結束時蜂鳴器報警提示。LCD顯示相關信息。 基本功能描述用單片機模擬實現洗衣機控制。用直流電機轉動模擬洗衣機運…

游戲引擎學習第290天:完成分離渲染

game_sim_region.cpp&#xff1a;在BeginSim中移除EntityOverlapsRectangle調用 現在我們接近一個關鍵點&#xff0c;雖然還沒完全結束&#xff0c;但我們已經把所有東西遷移到了一個新概念上——即那些臨時創建的控制器結構&#xff0c;稱為“腦”&#xff08;brains&#xf…

JavaScript性能優化實戰(12):大型應用性能優化實戰案例

在前面的系列文章中,我們探討了各種JavaScript性能優化技術和策略。本篇將聚焦于實際的大型應用場景,通過真實案例展示如何綜合運用這些技術,解決復雜應用中的性能挑戰。 目錄 電商平臺首屏加載優化全流程復雜數據可視化應用性能優化案例在線協作工具的實時響應優化移動端W…

Linux 安裝 Unreal Engine

需要對在unreal engine官網進行綁定github賬號&#xff0c;然后到unreal engine github倉庫中進行下載對應的版本&#xff0c;并進行安裝unreal engine官網 github地址

2.2.4

import pandas as pd from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score import joblib from xgboost import XGBRegressor # 加載數據集 file_path 大學…

使用IDEA創建Maven版本的web項目以及lombok的使用

1.新建項目 2.修改pom.xml 3.修改項目結構 4.在main/java下面寫一個Servlet測試一下 然后當前頁面往下滑 -Dfile.encodingUTF-8編寫一句輸出語句&#xff0c;測試是否成功部署配置&#xff0c;并選擇到正確的位置&#xff1a; 回車以后 再回到idea里面&#xff0c;發現控…

【數據結構】1-3 算法的時間復雜度

數據結構知識點合集&#xff1a;數據結構與算法 ? 知識點 ? 時間復雜度的定義 1、算法時間復雜度 事前預估算法時間開銷T(n)與問題規模 n 的關系&#xff08;T 表示 “time”&#xff09; 2、語句頻度 算法中語句的執行次數 對于以上算法&#xff0c;語句頻度&#xff1a;…

【Python 算法零基礎 3.遞推】

壓抑與痛苦&#xff0c;那些輾轉反側的夜&#xff0c;終會讓我們更加強大 —— 25.5.16 一、遞推的概念 遞推 —— 遞推最通俗的理解就是數列&#xff0c;遞推和數列的關系就好比 算法 和 數據結構 的關系&#xff0c;數列有點像數據結構中的線性表(可以是順序表&#xff0c;也…

淘寶扭蛋機系統開發前景分析:解鎖電商娛樂化新藍海

在電商行業競爭日益白熱化的當下&#xff0c;如何通過創新玩法提升用戶粘性、激活消費潛力&#xff0c;成為平臺突破增長瓶頸的關鍵。淘寶扭蛋機系統作為“電商娛樂”的跨界融合產物&#xff0c;正憑借其趣味性、隨機性和社交屬性&#xff0c;成為撬動年輕消費市場的潛力工具。…

NHANES指標推薦:UHR

文章題目&#xff1a;Elevated log uric acid-to-high-density lipoprotein cholesterol ratio (UHR) as a predictor of increased female infertility risk: insights from the NHANES 2013-2020 DOI&#xff1a;10.1186/s12944-025-02521-w 中文標題&#xff1a;對數尿酸與高…