EN

Android 基于 Choreographer 的渲染机制详解

字数统计: 8.4k阅读时长: 35 min
2019/10/22
loading

本文介绍了 App 开发者不经常接触到但是在 Android Framework 渲染链路中非常重要的一个类 Choreographer。包括 Choreographer 的引入背景、Choreographer 的简介、部分源码解析、Choreographer 与 MessageQueue、Choreographer 和 APM,以及手机厂商基于 Choreographer 的一些优化思路

Choreographer 让动画、部分输入处理和 View 更新围绕显示时序协调执行。它在有待处理工作时请求帧回调,而不是每个物理刷新周期都无条件唤醒 App。2019 年常见的 60Hz 屏幕刷新间隔约为 16.67ms;90Hz、120Hz 分别约为 11.11ms、8.33ms,现代设备还可能动态切换刷新率。回调及时不等于画面已经呈现,后续还涉及 RenderThread、GPU 和合成显示链路。了解这套流程,也有助于理解 Message、Handler、Looper、MessageQueue、Measure、Layout 和 Draw。

版本说明(2026-10-13 技术复核): 旧文源码摘录以 AOSP android-10.0.0_r1 的 Choreographer 为参照,保留 2019 年截图;补充 Android 16 实现和 API 24/31/33 的观测入口。源码节选不是独立可编译应用,厂商实现和截图配置未完整提供;旧私有接口已在相应章节标注。

本文是 Systrace 系列文章的第八篇,主要是对 Systrace 中的 Choreographer 进行简单介绍

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Triple Buffer 解读
  11. Systrace 基础知识 - CPU Info 解读
  12. Systrace 流畅性实战 1 :了解卡顿原理
  13. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  14. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  15. Systrace 响应速度实战 1 :了解响应速度原理
  16. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  17. Systrace 响应速度实战 3 :响应速度延伸知识
  18. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  19. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

主线程运行机制的本质

这篇文章对应后续的 Perfetto 5:Android App 基于 Choreographer 的渲染流程。Android 主线程通过 Looper 分发 Message;不是 MessageQueue 自己执行应用代码。等待下一条消息时,native poll 也可能处理文件描述符事件,VSync 接收正与此有关,所以“主线程所有操作都是 Message”只是过度简化。

在 Perfetto 中,把问题帧与主线程、RenderThread、GPU 和 SurfaceFlinger 对齐。Android 12 / API 31 及以上,且采集包含对应数据时,可利用 FrameTimeline 的 expected/actual 轨道定位超期;缺轨道不等于零掉帧。线程 Sleep、Runnable 和 Running 要分别调查,不能把 slice 墙钟时长直接当 CPU 时间。下图展示的是一个有间隔的历史分发过程,MethodTrace 插桩也会影响时间。

MethodTrace 图示

Systrace 图示

演进

Choreographer 在 Android 4.1 / API 16 引入,用来协调显示时序上的工作。下面两张原图中已经有 Choreographer.doFrame 和 RenderThread,因此不能把它们当成“引入 Choreographer 之前的 Android”证据。它们展示了另一组较密集的渲染和等待;具体系统配置没有随图提供。

MethodTrace 图示

包含 Choreographer.doFrame 的历史方法轨迹

出现 dequeueBuffer 区间的历史 Trace

图中能看到 RenderThread 的 dequeueBuffer 区间,但仅凭这张图不能证明唯一瓶颈或固定消费速率。可用 Buffer、消费者进度和 fence 等待都需要进一步核对;不是 SurfaceFlinger 以绝对固定速度直接消费一个“Front Buffer”。

稳定的呈现节奏很重要,但“稳定 50fps 一定比 40–60fps 波动更流畅”不能一概而论。在固定 60Hz 显示上,50fps 本身也无法逐帧按整数刷新间隔均匀呈现。VSync 协调与缓冲机制能改善 pacing,并不保证稳定帧率或消除工作量。

引入 Choreographer

Choreographer 请求下一次适用的 VSync 事件,由绑定 Looper 的线程调度回调。它不负责自行改变屏幕刷新率,App 的 VSync 回调也不是直接运行在硬件中断上下文。没有待处理工作时,普通 View 窗口不需要持续产生新帧。

历史渲染示意图:RenderThread 是 CPU 线程,16.6ms 只是 60Hz 示例

这张原示意图需要按简化图阅读:RenderThread 是 CPU 线程,图中把它的工作框整体写成“GPU”不准确;获取 Buffer、提交 GPU 命令、入队和合成并非图上每格都严格串行。16.6ms 只是当时 60Hz 的近似刷新间隔,不代表任意一帧的所有阶段都在这一段内完成,也不是端到端输入延迟。高刷背景可参考90Hz 漫谈。

Choreographer 简介

Choreographer 接收有时限的帧回调,按阶段执行到期工作,并请求下一次需要的显示事件。Android 10 的顺序是 INPUT → ANIMATION → INSETS_ANIMATION → TRAVERSAL → COMMIT;这不是把 App 的所有 Message 都收进来,也不意味着每帧必做完整 measure/layout/draw。

Choreographer、SurfaceFlinger 和缓冲机制共同参与帧调度,但不能保证 App 固定达到 20、60 或 90fps。监控中还要区分公开 FrameCallback、隐藏的 FrameInfo、Looper 消息计时与最终呈现数据。IdleHandler 在消息队列满足空闲条件时运行,不是“每帧结束”的固定通知。

另外虽然画图是一个比较好的解释流程的好路子,但是我个人不是很喜欢画图,因为平时 Systrace 和 MethodTrace 用的比较多,Systrace 是按从左到右展示整个系统的运行情况的一个工具(包括 cpu、SurfaceFlinger、SystemServer、App 等关键进程),使用 Systrace 和 MethodTrace 也可以很方便地展示关键流程。当你对系统代码比较熟悉的时候,看 Systrace 就可以和手机运行的实际情况对应起来。所以下面的文章除了一些网图之外,其他的我会多以 Systrace 来展示。

从 Systrace 的角度来看 Choreogrepher 的工作流程

下图以桌面滑动为例。绿色 F 是旧版 Systrace 的帧分析标记,不能独自证明这一帧最终显示成功。灰白背景代表该 Trace 的 VSync 参考间隔,具体数值要看时间轴和采集配置,不能从颜色直接认定 16.6ms。

典型硬件加速 View 路径中,主线程更新绘制状态,RenderThread 准备并提交渲染工作;Buffer 入队后还要与 GPU 完成、SurfaceFlinger 合成和实际呈现关联。UI/RT 可以重叠,不能简单将两段墙钟时长相加作为显示耗时。

有了上面这个整体的概念,我们将 UI Thread 的每一帧放大来看,看看 Choreogrepher 的位置以及 Choreogrepher 是怎么组织每一帧的

Choreographer 的工作流程

  1. 初始化线程内 Choreographer,绑定 Looper/FrameHandler,创建显示事件接收器与回调队列。
  2. 有到期工作且尚未安排帧时,请求 VSync;事件到达后,onVsync 将一个异步 Runnable 放入同一 Looper 的队列,之后 run() 执行 doFrame()。
  3. doFrame 核对帧是否仍需要执行,计算开始处理相对帧时间的延迟,必要时校正动画帧时间。
  4. 依次执行到期的 INPUT、ANIMATION、INSETS_ANIMATION、TRAVERSAL、COMMIT 回调。Traversal 中可能把绘制状态同步给 RenderThread。
  5. RenderThread 提交 GPU 工作与 Buffer;提交返回不表示 GPU 已完成。相关 fence 描述工作完成/使用约束,SurfaceFlinger/HWC 后续才能按依赖安排合成和显示。
  6. 动画等仍有后续工作时再次安排帧;并非无条件永久循环。

上图的旧标注把 Traversal 写作第三项,省略了 Insets Animation;Android 10 源码中它是第四类。图中“渲染完成抛 Buffer”也应按上述异步 GPU/fence 边界理解。

同时也附上这一帧所对应的 MethodTrace(这里预览一下即可,下面会有详细的大图)

下面我们就从源码的角度,来看一下具体的实现

源码解析

以下为 Android 10 的源码节选,注释中的省略部分包含其他检查和状态维护,不可把多个片段拼成完整类。Android 16 的 doFrame 已接收 VsyncEventData,使用事件携带的 frame interval、vsync id 和 deadline 等信息;签名与旧片段不同,见版本化源码。

Choreographer 的初始化

Choreographer 的单例初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Thread local storage for the choreographer.
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
// 获取当前线程的 Looper
Looper looper = Looper.myLooper();
if (looper == null) {
throw new IllegalStateException("The current thread must have a looper!");
}
// 构造 Choreographer 对象
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
mMainInstance = choreographer;
}
return choreographer;
}
};

Choreographer 的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
// 1. 初始化 FrameHandler
mHandler = new FrameHandler(looper);
// 2. 初始化 DisplayEventReceiver
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
//3. 初始化 CallbacksQueues
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
// ... other source omitted
}

FrameHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private final class FrameHandler extends Handler {
// ... other source omitted
public 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://处理 Callback
doScheduleCallback(msg.arg1);
break;
}
}
}

Choreographer 初始化链

新窗口的 Activity 恢复显示过程中,窗口添加路径可以创建 ViewRootImpl,并取得当前线程的 Choreographer。它也可能被动画等更早初始化,不能把这条链当成唯一创建时机;已有 ViewRootImpl 的恢复也不必重建。下面是调用关系示意:

1
2
3
4
5
6
7
8
9
ActivityThread.handleResumeActivity(IBinder, boolean, boolean, String) (android.app) 
-->WindowManagerImpl.addView(View, LayoutParams) (android.view) 
-->WindowManagerGlobal.addView(View, LayoutParams, Display, Window) (android.view) 
-->ViewRootImpl.ViewRootImpl(Context, Display) (android.view) 
public ViewRootImpl(Context context, Display display) {
// ... other source omitted
mChoreographer = Choreographer.getInstance();
// ... other source omitted
}

FrameDisplayEventReceiver 简介

Vsync 的注册、申请、接收都是通过 FrameDisplayEventReceiver 这个类,所以可以先简单介绍一下。 FrameDisplayEventReceiver 继承 DisplayEventReceiver , 有三个比较重要的方法

  1. onVsync – Vsync 信号回调
  2. run – 执行 doFrame
  3. scheduleVsync – 请求 Vsync 信号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
// ... other source omitted
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
// ... other source omitted
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}

}

scheduleVsync() 的实现位于父类 DisplayEventReceiver,不应把访问其私有 mReceiverPtr 的代码直接搬进子类。FrameHandler 中的 MSG_DO_FRAME 分支主要是禁用 VSync 时的定时回退;正常 VSync 路径使用上面这个 Runnable。

Choreographer 中 Vsync 的注册

从下面的函数调用栈可以看到,Choreographer 的内部类 FrameDisplayEventReceiver.onVsync 负责接收 Vsync 回调,通知 UIThread 进行数据处理。

那么 FrameDisplayEventReceiver 是通过什么方式在 Vsync 信号到来的时候回调 onVsync 呢?答案是 FrameDisplayEventReceiver 的初始化的时候,最终通过监听文件句柄的形式,其对应的初始化流程如下

android/view/Choreographer.java

1
2
3
4
5
6
7
private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
// ... other source omitted
}

android/view/Choreographer.java

1
2
3
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}

android/view/DisplayEventReceiver.java

1
2
3
4
5
6
public DisplayEventReceiver(Looper looper, int vsyncSource) {
// ... other source omitted
mMessageQueue = looper.getQueue();
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
vsyncSource);
}

这里要区分控制请求和事件传送。DisplayEventReceiver::requestNextVsync()通过 IDisplayEventConnection 的 Binder 方法请求下一次 VSync;事件则从 BitTube 通道读回。不是“请求和回调都通过同一个 socket 发消息”。

DisplayEventDispatcher::initialize()把接收端 FD 加入 native Looper。FD 可读时分发事件到 Java onVsync(),后者再投递异步 Runnable,因此 onVsync 与 doFrame 是两步,不是 App 执行硬件中断。原图展示了这两个阶段:

Choreographer 处理一帧的逻辑

Choreographer 处理绘制的逻辑核心在 Choreographer.doFrame 函数中,从下图可以看到,FrameDisplayEventReceiver.onVsync post 了自己,其 run 方法直接调用了 doFrame 开始一帧的逻辑处理

android/view/Choreographer.java

1
2
3
4
5
6
7
8
9
10
11
12
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
// ... other source omitted
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}

doFrame 函数主要做下面几件事

  1. 估计开始处理相对帧时间的延迟
  2. 记录帧绘制信息
  3. 执行 CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_INSETS_ANIMATION、CALLBACK_TRAVERSAL、CALLBACK_COMMIT

计算掉帧逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
// ... other source omitted
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
}
// ... other source omitted
}
// ... other source omitted
}

这里计算的是 startNanos - frameTimeNanos:当前 doFrame 开始处理时,距离所携带的帧时间过去了多久。整数除以该版本的帧间隔,得到错过的调度间隔数;它不是这帧绘制耗时,也不是完整的实际呈现掉帧数。源码还会调整用于动画的 frameTimeNanos,并处理时间倒退等情况。

历史时序图:应按帧时间戳和实际间隔理解 jitter,不能固定除以16ms

图中 start_time 应理解为事件携带的帧时间戳,不是 Java 收到回调那一刻的系统时间;分母也不能固定为 16ms。

前一次主线程工作拖延后续帧处理的历史示例

前一段长工作可能拖延下一次回调,但“这只是在算前一帧掉帧”并不准确。GPU、合成或呈现超期可能没有对应的这条警告,主线程处理延迟也可能来自 Runnable 调度等待,而不全是业务代码在 CPU 上忙。图中最后一处 start_time - end_time 还写反了,源码计算方向为开始处理时间减帧时间戳。

记录帧绘制信息

Choreographer 使用内部 android.graphics.FrameInfo记录部分阶段时间,HWUI 再补充后续字段。它带 @hide,不是公开给普通 App 的稳定 SDK 类。阶段开始时间也不是每个业务回调的独立耗时;下面是 Android 10 的数组索引,不能用固定下标解析所有新版输出。

从 FrameInfo 这些标志就可以看出记录的内容,后面我们看 dumpsys gfxinfo 的时候数据就是按照这个来排列的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Various flags set to provide extra metadata about the current frame
private static final int FLAGS = 0;

// Is this the first-draw following a window layout?
public static final long FLAG_WINDOW_LAYOUT_CHANGED = 1;

// A renderer associated with just a Surface, not with a ViewRootImpl instance.
public static final long FLAG_SURFACE_CANVAS = 1 << 2;

@LongDef(flag = true, value = {
FLAG_WINDOW_LAYOUT_CHANGED, FLAG_SURFACE_CANVAS })
@Retention(RetentionPolicy.SOURCE)
public @interface FrameInfoFlags {}

// The intended vsync time, unadjusted by jitter
private static final int INTENDED_VSYNC = 1;

// Jitter-adjusted vsync time, this is what was used as input into the
// animation & drawing system
private static final int VSYNC = 2;

// The time of the oldest input event
private static final int OLDEST_INPUT_EVENT = 3;

// The time of the newest input event
private static final int NEWEST_INPUT_EVENT = 4;

// When input event handling started
private static final int HANDLE_INPUT_START = 5;

// When animation evaluations started
private static final int ANIMATION_START = 6;

// When ViewRootImpl#performTraversals() started
private static final int PERFORM_TRAVERSALS_START = 7;

// When View:draw() started
private static final int DRAW_START = 8;

doFrame 函数记录从 Vsync time 到 markPerformTraversalsStart 的时间

1
2
3
4
5
6
7
8
9
10
11
12
13
void doFrame(long frameTimeNanos, int frame) {
// ... other source omitted
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
// 处理 CALLBACK_INPUT Callbacks
mFrameInfo.markInputHandlingStart();
// 处理 CALLBACK_ANIMATION Callbacks
mFrameInfo.markAnimationsStart();
// 处理 CALLBACK_INSETS_ANIMATION Callbacks
// 处理 CALLBACK_TRAVERSAL Callbacks
mFrameInfo.markPerformTraversalsStart();
// 处理 CALLBACK_COMMIT Callbacks
// ... other source omitted
}

执行 Callbacks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void doFrame(long frameTimeNanos, int frame) {
// ... other source omitted
// 处理 CALLBACK_INPUT Callbacks
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
// 处理 CALLBACK_ANIMATION Callbacks
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
// 处理 CALLBACK_INSETS_ANIMATION Callbacks
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
// 处理 CALLBACK_TRAVERSAL Callbacks
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
// 处理 CALLBACK_COMMIT Callbacks
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
// ... other source omitted
}

Input 回调调用栈

input callback 一般是执行 ViewRootImpl.ConsumeBatchedInputRunnable

android/view/ViewRootImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final class ConsumeBatchedInputRunnable implements Runnable {
@Override
public void run() {
doConsumeBatchedInput(mChoreographer.getFrameTimeNanos());
}
}
void doConsumeBatchedInput(long frameTimeNanos) {
if (mConsumeBatchedInputScheduled) {
mConsumeBatchedInputScheduled = false;
if (mInputEventReceiver != null) {
if (mInputEventReceiver.consumeBatchedInputEvents(frameTimeNanos)
&& frameTimeNanos != -1) {
scheduleConsumeBatchedInput();
}
}
doProcessInputEvents();
}
}

这里展示的是批量输入消费后进入 DecorView/Activity/View 事件分发的一个路径。并非所有输入都要等待 INPUT 回调,按键、非批量事件及其他分发路径要分别分析。

Animation 回调调用栈

一般我们接触的多的是调用 View.postOnAnimation 的时候,会使用到 CALLBACK_ANIMATION

1
2
3
4
5
6
7
8
9
10
11
public void postOnAnimation(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.mChoreographer.postCallback(
Choreographer.CALLBACK_ANIMATION, action, null);
} else {
// Postpone the runnable until we know
// on which thread it needs to run.
getRunQueue().post(action);
}
}

那么一般是什么时候回调用到 View.postOnAnimation 呢,我截取了一张图,大家可以自己去看一下,接触最多的应该是 startScroll,Fling 这种操作

具体调用栈取决于所提交的回调。下面图中能看到 AnimationHandler、ValueAnimator/AnimatorSet 等动画计算;仅凭这张图不能把它确定成 AbsListView 的 FlingRunnable。

另外我们的 Choreographer 的 FrameCallback 也是用的 CALLBACK_ANIMATION

1
2
3
4
5
6
7
8
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}

postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

Traversal 调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 先插入同步屏障,异步消息仍可被选取
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}

final class TraversalRunnable implements Runnable {
@Override
public void run() {
// 进入 traversal,按条件测量、布局和绘制
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 这里把 SyncBarrier remove
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
// 进入 traversal
performTraversals();
}
}
// Simplified outline; surrounding state and other branches are omitted.
private void performTraversals() {
// measure 操作
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight() || contentInsetsChanged || updatedConfiguration) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// layout 操作
if (didLayout) {
performLayout(lp, mWidth, mHeight);
}
// draw 操作
if (!cancelDraw && !newSurface) {
performDraw();
}
}

以上 ViewRootImpl 片段只保留条件分支骨架,局部变量来自省略的外围逻辑。不是每帧都执行 measure/layout/draw;屏障也不提升 CPU 调度优先级,其作用见后面的 MessageQueue 章节。

doTraversal 的 TraceView 示例

下一帧的 Vsync 请求

由于动画、滑动、Fling 这些操作的存在,我们需要一个连续的、稳定的帧率输出机制。这就涉及到了 Vsync 的请求逻辑,在连续的操作,比如动画、滑动、Fling 这些情况下,每一帧的 doFrame 的时候,都会根据情况触发下一个 Vsync 的申请,这样我们就可以获得连续的 Vsync 信号。

看下面的 scheduleTraversals 调用栈(scheduleTraversals 中会触发 Vsync 请求)

invalidate/requestLayout 可以促成 traversal 调度,但请求会合并,且取决于 View 状态等条件,不是每调用一次就各产生一个 VSync。

我们下面以 Animation 为例,看看 Animation 是如何驱动下一个 Vsync ,来持续更新画面的

ObjectAnimator 动画驱动逻辑

android/animation/ObjectAnimator.java

1
2
3
public void start() {
super.start();
}

android/animation/ValueAnimator.java

1
2
3
4
5
6
7
8
9
private void start(boolean playBackwards) {
// ... other source omitted
addAnimationCallback(0); // 动画 start 的时候添加 Animation Callback
// ... other source omitted
}
private void addAnimationCallback(long delay) {
// ... other source omitted
getAnimationHandler().addAnimationFrameCallback(this, delay);
}

android/animation/AnimationHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
if (mAnimationCallbacks.size() == 0) {
// post FrameCallback
getProvider().postFrameCallback(mFrameCallback);
}
// ... other source omitted
}

// 这里的 mFrameCallback 回调 doFrame,里面 post了自己
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
doAnimationFrame(getProvider().getFrameTime());
if (mAnimationCallbacks.size() > 0) {
// post 自己
getProvider().postFrameCallback(this);
}
}
};

调用 postFrameCallback 会走到 mChoreographer.postFrameCallback ,这里就会触发 Choreographer 的 Vsync 请求逻辑

android/animation/AnimationHandler.java

1
2
3
public void postFrameCallback(Choreographer.FrameCallback callback) {
mChoreographer.postFrameCallback(callback);
}

android/view/Choreographer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 ->scheduleVsyncLocked-> mDisplayEventReceiver.scheduleVsync ->nativeScheduleVsync
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

上述动画循环在回调列表仍有待执行工作时重新 post。公开的 FrameCallback 单次执行后会移除,连续动画由 AnimationHandler 主动续订;普通 App 自己写循环也要在停止/不可见时取消。
动画过程中一帧的 TraceView 示例

源码小结

  1. getInstance() 取得当前 Looper 线程的 Choreographer,不是整个进程唯一一个;无 Looper 时会抛异常。显示事件和 doFrame 都在绑定线程上处理。
  2. VSync 请求走显示连接,事件经 FD/Looper 分发,再以异步 Message 执行 doFrame;不是在 App 中处理显示硬件中断。
  3. FrameCallback 单次有效,不能当成自动常驻订阅。FrameCallback 位于 ANIMATION 阶段,执行一次不保证生成新 Buffer。
  4. Android 10/16 都按 INPUT、ANIMATION、INSETS_ANIMATION、TRAVERSAL、COMMIT 顺序处理到期队列;前面阶段的状态变化可以影响后面的 traversal。
  5. COMMIT 在 traversal 后执行,不是 GPU/SurfaceFlinger 完成或实际呈现通知。ActivityThread.scheduleTrimMemory()会在有主线程 Choreographer 时把内存整理安排到这一阶段,但这只是用途之一,也不是每帧都会调用 onTrimMemory。
  6. ListView 的 obtain/setup 可能由输入滚动或动画继续滚动触发,也可能出现在布局路径。应根据实际栈区分,不能只按阶段名猜调用原因。

APM 与 Choreographer

不同入口观测的是不同阶段,不能把所有数字都叫“FPS”或“掉帧数”:

  • FrameCallback:观察所在 Looper 的帧回调节奏,不是 Buffer/呈现计数。
  • dumpsys gfxinfo ... framestats:查看对应窗口的 HWUI 帧统计;按该设备输出的字段解释。
  • SurfaceFlinger --latency:旧版的帧时间统计,需匹配真实 layer 和系统实现。
  • PageFlip 私有事务:仅保留为历史说明,Android 16 的旧查询已不可用。
  • Choreographer skip warning:开始处理的延迟提示,非完整呈现判据。
  • Looper/BlockCanary:消息分发耗时,非帧显示耗时。

现代入口中,Android 7 / API 24 提供 Window.OnFrameMetricsAvailableListener;回调通知也可能丢失,dropCountSinceLastInvocation 不是画面掉帧数。Android 12 / API 31 起,在有数据的设备/采集中可用 Perfetto FrameTimeline关联 expected/actual 和应用/SF 帧,继续定位 CPU、GPU 或合成延迟。

利用 FrameCallback 的 doFrame 回调

FrameCallback 接口

1
2
3
public interface FrameCallback {
void doFrame(long frameTimeNanos);
}

接口使用

以下代码应在已有 Looper 的线程上使用;普通 View 应用通常是主线程:

1
2
3
4
5
6
7
Choreographer choreographer = Choreographer.getInstance();
Choreographer.FrameCallback callback = frameTimeNanos -> {
// Update animation state using this frame time; keep the work short.
};
choreographer.postFrameCallback(callback);
// If no longer needed before delivery, cancel on the same Looper thread:
// choreographer.removeFrameCallback(callback);

接口处理

1
2
3
// Android 10: postFrameCallbackDelayed() delegates to this internal queue.
postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);

postFrameCallback() 从 API 16 起公开,每次只调用一次。持续观察需要再次 post 并管理停止条件;这种探针本身也会继续请求帧,不能把由它驱动的回调次数等同于 App 实际绘制次数。frameTimeNanos 是框架给出的动画帧时间,可能经过延迟校正,不等同于回调实际开始时间。

TinyDancer是原文参考的历史项目,使用 Choreographer 回调估计节奏;它展示的数字不能直接代表合成后屏幕 FPS。API 33 还增加 postVsyncCallback(),可在回调内通过 FrameData/FrameTimeline 读取候选 deadline 和 expected presentation time;这些是调度预测,不是实际呈现测量,数据也不应脱离回调生命周期持有使用。

利用 FrameInfo 进行监控

1
adb shell dumpsys gfxinfo com.meizu.flyme.launcher framestats

包名应替换为现场目标。下面保留原始样本;其窗口标为 StatusBar,不能声称它就是上面 Launcher 命令在同次复现中的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Window: StatusBar
Stats since: 17990256398ns
Total frames rendered: 1562
Janky frames: 361 (23.11%)
50th percentile: 6ms
90th percentile: 23ms
95th percentile: 36ms
99th percentile: 101ms
Number Missed Vsync: 33
Number High input latency: 683
Number Slow UI thread: 273
Number Slow bitmap uploads: 8
Number Slow issue draw commands: 18
Number Frame deadline missed: 287
HISTOGRAM: 5ms=670 6ms=128 7ms=84 8ms=63 9ms=38 10ms=23 11ms=21 12ms=20 13ms=25 14ms=39 15ms=65 16ms=36 17ms=51 18ms=37 19ms=41 20ms=20 21ms=19 22ms=18 23ms=15 24ms=14 25ms=8 26ms=4 27ms=6 28ms=3 29ms=4 30ms=2 31ms=2 32ms=6 34ms=12 36ms=10 38ms=9 40ms=3 42ms=4 44ms=5 46ms=8 48ms=6 53ms=6 57ms=4 61ms=1 65ms=0 69ms=2 73ms=2 77ms=3 81ms=4 85ms=1 89ms=2 93ms=0 97ms=2 101ms=1 105ms=1 109ms=1 113ms=1 117ms=1 121ms=2 125ms=1 129ms=0 133ms=1 150ms=2 200ms=3 250ms=0 300ms=1 350ms=1 400ms=0 450ms=0 500ms=0 550ms=0 600ms=0 650ms=0

---PROFILEDATA---
Flags,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,HandleInputStart,AnimationStart,PerformTraversalsStart,DrawStart,SyncQueued,SyncStart,IssueDrawCommandsStart,SwapBuffers,FrameCompleted,DequeueBufferDuration,QueueBufferDuration,
0,10158314881426,10158314881426,9223372036854775807,0,10158315693363,10158315760759,10158315769821,10158316032165,10158316627842,10158316838988,10158318055915,10158320387269,10158321770654,428000,773000,
0,10158332036261,10158332036261,9223372036854775807,0,10158332799196,10158332868519,10158332877269,10158333137738,10158333780654,10158333993206,10158335078467,10158337689561,10158339307061,474000,885000,
0,10158348665353,10158348665353,9223372036854775807,0,10158349710238,10158349773102,10158349780863,10158350405863,10158351135967,10158351360446,10158352300863,10158354305654,10158355814509,471000,836000,
0,10158365296729,10158365296729,9223372036854775807,0,10158365782373,10158365821019,10158365825238,10158365975290,10158366547946,10158366687217,10158367240706,10158368429248,10158369291852,269000,476000,

不要省略 CSV 表头:不同 Android/HWUI 版本可能增加或调整字段。时间戳以对应实现的单调时钟为基准,单位纳秒;Duration 字段是时长,不能把整行每列都当时间点。旧 OldestInputEvent=9223372036854775807 与 NewestInputEvent=0 是无有效输入时间的哨兵组合,不是巨大的输入延迟。FrameCompleted 不等于屏幕实际呈现时刻;各类慢帧计数可能重叠,也不应相加作为不同坏帧的总数。

利用 SurfaceFlinger 进行监控

下面是 Android 10 时代的诊断入口示例,先列出实际 layer,再查询目标;名称会随窗口、重建及版本变化:

1
2
adb shell dumpsys SurfaceFlinger --list
adb shell dumpsys SurfaceFlinger --latency 'com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher#0'

按 Android 10 SurfaceFlinger::dumpStatsLocked()和 FrameTracker::dumpStats():第一行为刷新间隔,后续三列按顺序为:

  1. A:desiredPresentTime,期望呈现时间。
  2. B:actualPresentTime,记录的实际呈现时间。
  3. C:frameReadyTime,帧内容可用于呈现的时间,可来自 ready fence。

它们不是“App 开始画”“提交硬件前 VSync”“硬件接受完”的三个时刻。该版 ring buffer 大小为 128,而每个 tracker 的 dump 循环输出 127 个槽位;零值、未就绪的 INT64_MAX 等不是有效帧。只统计目标、时间范围和有效记录,先验证单调性、缺失和刷新率变化,不能假设每次命令得到 128 个新帧。

原文的 ceil((C - A) / refresh_period) 及“相邻值变化就是掉帧”不构成通用判据:期望、就绪和实际呈现是不同阶段,稳定的长延迟甚至可能让该值保持不变。应根据该版本的数据定义分析实际呈现间隔/预期 deadline;支持时优先关联 FrameTimeline,不能沿用公式自动认定 jank。

利用 SurfaceFlinger PageFlip 机制进行监控

历史接口,不能照搬为当前 App 监控方案。 Android 10 私有调试事务 1013 返回默认显示的 PageFlip 计数;即使在允许访问的系统上,它也是显示级计数,不是某个 App 的帧数。原文命令还少了 shell 层;补上 adb shell service call SurfaceFlinger 1013 也不改变其私有、受权限控制的性质。

Android 16 源码已明确:

1
2
case 1013: // Unused.
return NAME_NOT_FOUND;

因此移除把 transact(1013) 的返回值直接读成计数并计算 App FPS 的旧示例,改用上面的公开窗口指标或有版本依据的 Trace 数据。不能忽略 transact/返回状态,也不能把不支持返回当作零帧。

Choreographer 自身的掉帧计算逻辑

Android 10 的 SKIPPED_FRAME_WARNING_LIMIT 默认是 30,由启动时读取的 debug.choreographer.skipwarning 属性配置。它控制日志阈值,并不意味着少于 30 个错过间隔就不卡,也不是每帧完整测量。

1
2
3
4
5
6
7
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
}

要区分帧时间、doFrame 实际开始、工作完成与最终呈现;改变告警阈值不会修复这些阶段的延迟。

BlockCanary

BlockCanary 一类工具利用 Looper 的消息分发前后日志做计时。下面摘录的是旧版分发边界:

1
2
3
4
5
6
7
8
9
10
// Excerpt inside Looper.loop(); obtaining the next message is omitted.
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

测得的是一次 dispatch 的墙钟时长,包括其中的等待;它不是纯 CPU 时间,不涵盖 RenderThread/GPU/SF 的完整链路,也不覆盖 dispatch 之外所有 native poll 回调。短消息连续排队造成的延迟也可能没有单条“长消息”。监控本身的字符串处理、日志和采样有开销,setMessageLogging 的使用还需与其他观察者协调。不要把日志监听等同于零开销、全覆盖的掉帧检测。

MessageQueue 与 Choreographer

在 Android 10 的 MessageQueue.next()里,当队首是 target == null 的同步屏障时,会向后查找异步消息,并仍检查它是否到达 when。屏障后的同步消息需等屏障移除;这不是抢占正在执行的消息,也不会提高线程的 CPU 调度优先级。

Message.setAsynchronous()从 API 22 起公开,原文“隐藏方法”的说法已过期;Handler.createAsync()从 API 28 起公开。异步表示可以越过同步屏障,不表示自动放到队首、另开线程或必定立即执行。没有屏障时,它仍受队列时间和顺序约束;框架的 post/removeSyncBarrier 本身不是普通 App 应依赖的公开 SDK。

SyncBarrier 在 Choreographer 中使用的一个示例

下面是 ViewRootImpl 的关联用法,省略其他状态维护。scheduleTraversals() 插入屏障并登记 traversal 回调;异步 VSync 消息可越过屏障,使 doFrame 有机会执行该回调:

1
2
3
4
5
6
7
8
9
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
// ... other source omitted
}
}

doTraversal() 在 performTraversals() 之前移除屏障,不是等待 GPU 绘完才移除:

1
2
3
4
5
6
7
8
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
// ... other source omitted
performTraversals();
}
}

异步消息只改变跨屏障的可执行性,不能绕过前面已经开始的长任务。不要用私有屏障或任意重排消息的方式修业务卡顿。

厂商优化

系统厂商由于可以直接修改源码,也利用这方面的便利,做一些功能和优化,不过由于保密的问题,代码就不直接放上来了,我可以大概说一下思路,感兴趣的可以私下讨论

移动事件优化

原文“Choreographer 本身没有 input”的说法需要区分:AOSP 已有 CALLBACK_INPUT 和批量输入消费;厂商可能尝试的是改变输入调度或更早响应的策略。本文没有公开对应代码和实验,不能将其当成 AOSP 标准能力。提前处理要同时评估事件合并、重采样、功耗和后续帧的呈现节奏,并非越早越好。

后台动画优化

应用不可见后,自己不断续订的 FrameCallback 或动画可能仍制造工作;是否需要停止,应结合窗口可见性、生命周期、多窗口和具体业务。不能把所有后台回调都判为无意义,也不能仅凭未被杀就推断它能无限制运行。

原文标注为后台场景的持续 doFrame,截图未提供独立可见性证据

这张图展示历史 com.netease.newsreader.activity 的密集帧活动。这是我当时记录的后台场景,截图本身没有独立可见性/功耗证据,也不能据此评价当前应用。应用侧先取消不再需要的动画和回调;系统策略还需验证可见窗口、功能正确性与调度限制。

帧绘制优化

“不等 VSync 就立即合成”是原文分享的厂商探索方向,不是普通 App 的公开开关。合成排期仍需满足 Buffer/fence、显示硬件和刷新约束;立即调度不保证降低端到端延迟,也可能增加功耗或破坏 pacing。没有提供实现与对照数据,因此不写成已验证优化。

应用启动优化

启动时应先找关键路径上的长工作、锁和排队。系统内部改变消息顺序要维护生命周期和依赖关系;任意把所谓重要消息放到队首会造成顺序错误或饥饿,不是通用提速方法。普通应用优先减少启动关键路径工作、延后非必要初始化,并核对实际首帧与可交互时间。

高帧率优化

60/90/120Hz 的刷新间隔分别约为 16.67/11.11/8.33ms,原文的 120Hz“8.8ms”不准确。刷新间隔也不是每个线程独占的完整预算。高刷策略需一起评估应用与游戏负载、帧率切换、温度、功耗和呈现节奏,而不能只要求所有 App 永远跑满刷新率。

参考资料

  1. https://www.jianshu.com/p/304f56f5d486
  2. http://gityuan.com/2017/02/25/choreographer/
  3. https://developer.android.com/reference/android/view/Choreographer
  4. https://www.jishuwen.com/d/2Vcc
  5. https://juejin.im/entry/5c8772eee51d456cda2e8099
  6. Android 开发高手课

本文知乎地址

由于博留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 基于 Choreographer 的渲染机制详解
掘金 - Android 基于 Choreographer 的渲染机制详解

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

CATALOG
  1. 1. 系列文章目录
  2. 2. 主线程运行机制的本质
    1. 2.1. 演进
    2. 2.2. 引入 Choreographer
  3. 3. Choreographer 简介
    1. 3.1. 从 Systrace 的角度来看 Choreogrepher 的工作流程
    2. 3.2. Choreographer 的工作流程
  4. 4. 源码解析
    1. 4.1. Choreographer 的初始化
      1. 4.1.1. Choreographer 的单例初始化
      2. 4.1.2. Choreographer 的构造函数
      3. 4.1.3. FrameHandler
      4. 4.1.4. Choreographer 初始化链
    2. 4.2. FrameDisplayEventReceiver 简介
    3. 4.3. Choreographer 中 Vsync 的注册
    4. 4.4. Choreographer 处理一帧的逻辑
      1. 4.4.1. 计算掉帧逻辑
      2. 4.4.2. 记录帧绘制信息
      3. 4.4.3. 执行 Callbacks
    5. 4.5. 下一帧的 Vsync 请求
      1. 4.5.1. ObjectAnimator 动画驱动逻辑
    6. 4.6. 源码小结
  5. 5. APM 与 Choreographer
    1. 5.1. 利用 FrameCallback 的 doFrame 回调
      1. 5.1.1. FrameCallback 接口
      2. 5.1.2. 接口使用
      3. 5.1.3. 接口处理
    2. 5.2. 利用 FrameInfo 进行监控
    3. 5.3. 利用 SurfaceFlinger 进行监控
    4. 5.4. 利用 SurfaceFlinger PageFlip 机制进行监控
    5. 5.5. Choreographer 自身的掉帧计算逻辑
    6. 5.6. BlockCanary
  6. 6. MessageQueue 与 Choreographer
    1. 6.1. SyncBarrier 在 Choreographer 中使用的一个示例
  7. 7. 厂商优化
    1. 7.1. 移动事件优化
    2. 7.2. 后台动画优化
    3. 7.3. 帧绘制优化
    4. 7.4. 应用启动优化
    5. 7.5. 高帧率优化
  8. 8. 参考资料
  9. 9. 本文知乎地址
  10. 10. 关于我 && 博客