【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.mzph.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;对数尿酸与高…