Choreographer
Choreographer是 Android 系统 UI 渲染流程的核心调度者,负责协调 UI 渲染的各个环节,保证页面绘制与屏幕刷新率同步,是解决 UI 卡顿、掉帧问题的关键核心类。
Choreographer是 Android UI 渲染的「编舞者」,核心作用是协调 UI 任务与 VSYNC 信号同步,保证 UI 流畅性。- 内部维护 4 个优先级任务队列(INPUT > ANIMATION > TRAVERSAL > COMMIT),按序执行确保核心任务优先完成。
- 常用
FrameCallback监听帧渲染,用于帧率监控与卡顿排查,是性能优化工具的核心基础。 - 卡顿的本质是「单个 VSYNC 周期内任务无法完成」,
Choreographer可帮助定位问题,但优化需聚焦于减少measure/layout/draw耗时与主线程阻塞。
Choreographer 核心定位与作用
1. 核心定位
Choreographer 直译是「编舞者」,位于 android.view 包下,是 Android 系统用于协调 UI 渲染、输入事件、动画执行的核心调度类,本质是一个「同步调度器」,将各类与 UI 相关的任务与屏幕的垂直同步信号(VSYNC)对齐。
2. 核心作用
- 接收 VSYNC 信号:监听屏幕硬件发出的垂直同步信号(VSYNC,Vertical Sync),该信号由屏幕刷新率决定(如 60Hz 屏幕每 16.67ms 发送一次,120Hz 屏幕每 8.33ms 发送一次)。
- 调度 UI 渲染任务:协调
Measure(测量)、Layout(布局)、Draw(绘制)、Commit(合成)等 UI 渲染环节,确保每一轮渲染任务都在一个 VSYNC 周期内完成,避免掉帧。 - 统一任务调度:将输入事件(如触摸、按键)、动画执行(如
ValueAnimator)、Traversal 任务(UI 刷新)统一纳入调度,保证任务执行的时序一致性,避免竞争资源导致的卡顿。 - 避免过度绘制与卡顿:通过与 VSYNC 信号同步,防止同一帧内多次执行 UI 刷新,减少 CPU/GPU 资源浪费,保证 UI 流畅性(目标是每帧耗时 ≤ VSYNC 周期,即 60Hz 下 ≤ 16.67ms)。
Choreographer 核心工作原理
1. 核心依赖:VSYNC 信号
屏幕显示的原理是「逐行扫描、逐帧刷新」,VSYNC 信号是硬件层面发出的同步触发信号,作用是通知 CPU/GPU「可以开始准备下一帧的渲染数据了」。
Choreographer通过DisplayEventReceiver注册 VSYNC 信号监听,当收到 VSYNC 信号时,触发后续的任务调度。- 没有 VSYNC 信号时,
Choreographer会挂起任务,避免无效的 CPU 消耗,这是系统省电与流畅的重要保障。
2. 任务队列:四大类型有序执行
Choreographer 内部维护了 4 个优先级不同的任务队列,收到 VSYNC 信号后,会按优先级从高到低依次执行队列中的任务,确保核心任务优先完成。
| 任务队列类型 | 优先级 | 核心任务 | 执行时机 |
|---|---|---|---|
INPUT(输入事件) | 最高 | 处理触摸、按键等用户输入事件 | 最先执行,保证用户交互的响应及时性 |
ANIMATION(动画) | 次高 | 执行属性动画、视图动画等 | 输入事件后执行,保证动画的流畅性 |
TRAVERSAL(UI 遍历) | 中等 | 执行 View 的 measure、layout、draw 流程 | 动画后执行,完成 UI 布局与绘制 |
COMMIT(提交) | 最低 | 提交渲染数据、更新视图树状态、清理资源 | 最后执行,保证渲染数据的最终提交 |
3. 核心工作流程(简化版)
- 任务入队:当应用触发 UI 刷新(如
View.invalidate()、Activity.onResume())、动画启动、用户输入时,相关任务会被加入对应优先级的任务队列。 - 注册 VSYNC 监听:如果当前没有注册 VSYNC 监听,
Choreographer会通过DisplayEventReceiver向系统注册下一个 VSYNC 信号监听。 - 接收 VSYNC 信号:屏幕发出 VSYNC 信号后,
DisplayEventReceiver回调onVsync()方法,通知Choreographer开始执行任务。 - 按优先级执行任务:
Choreographer依次执行INPUT→ANIMATION→TRAVERSAL→COMMIT队列中的任务,其中TRAVERSAL队列的任务会触发ViewRootImpl.performTraversals(),进而执行 View 树的measure、layout、draw。 - 提交渲染数据:任务执行完成后,将绘制好的帧数据提交给 GPU,由 GPU 合成后通过
SurfaceFlinger显示到屏幕上。 - 循环等待下一个 VSYNC:如果还有未执行的任务,重复步骤 2-5,否则挂起,等待下一次任务入队。
关键 API 与实际应用
1. 核心 API (开发者常用)
(1) 获取 Choreographer 实例
Choreographer 采用单例模式,每个 Looper 对应一个 Choreographer 实例(主线程 Looper 对应 UI 渲染的核心实例),无法直接 new,只能通过静态方法获取:
// 获取当前线程对应的 Choreographer 实例(主线程调用即为 UI 渲染的核心实例)
val choreographer = Choreographer.getInstance()
(2) 监听帧回调(排查卡顿、监控帧率)
Choreographer 提供 postFrameCallback() 方法,可监听每一次 VSYNC 信号触发的帧渲染,常用于监控帧率、排查 UI 卡顿(如 BlockCanary、Trace Canary 均基于此实现)。
示例:监控帧率与每帧耗时
private var lastFrameTimeNanos: Long = 0
/**
* 注册帧回调,监控帧率与每帧耗时
*/
private fun monitorFrameRate() {
val choreographer = Choreographer.getInstance()
choreographer.postFrameCallback(object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
// frameTimeNanos:当前帧的 VSYNC 信号时间戳(纳秒)
if (lastFrameTimeNanos != 0L) {
// 计算两帧之间的耗时(毫秒)
val frameCostMs = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000.0
// 计算帧率(理论值,实际需平滑处理)
val frameRate = 1000.0 / frameCostMs
// 打印日志:每帧耗时与帧率
android.util.Log.d("ChoreographerDemo", "每帧耗时:${String.format("%.2f", frameCostMs)}ms,帧率:${String.format("%.0f", frameRate)}fps")
// 判定卡顿:若每帧耗时 > 16.67ms(60Hz 屏幕),则视为掉帧/卡顿
if (frameCostMs > 16.67) {
android.util.Log.w("ChoreographerDemo", "出现卡顿,当前帧耗时:${String.format("%.2f", frameCostMs)}ms")
}
}
// 更新上一帧的时间戳
lastFrameTimeNanos = frameTimeNanos
// 重新注册回调,实现持续监控(取消监控可停止调用此方法)
choreographer.postFrameCallback(this)
}
})
}
(3) 提交任务到指定队列
可通过 postCallback() 方法将自定义任务提交到 Choreographer 的指定优先级队列,确保任务与 UI 渲染时序对齐:
// 提交任务到 ANIMATION 队列,与动画任务同步执行
Choreographer.getInstance().postCallback(
Choreographer.CALLBACK_ANIMATION, // 任务队列类型
Runnable {
// 自定义任务:如更新动画相关数据
android.util.Log.d("ChoreographerDemo", "执行动画队列自定义任务")
},
null // 额外参数,可传递任务相关数据
)
2. 实际应用场景
- 帧率监控与卡顿排查:如 BlockCanary、Trace Canary 等工具,通过
FrameCallback监听每帧耗时,定位主线程阻塞导致的掉帧。 - 动画流畅性优化:将动画相关任务提交到
ANIMATION队列,确保动画与 VSYNC 信号同步,避免动画抖动。 - 自定义 View 渲染优化:在自定义 View 中,通过
Choreographer调度刷新任务,避免频繁调用invalidate()导致过度绘制。 - 性能测试工具:如 Android Studio Frame Profiler,通过监听
Choreographer的任务执行,分析每帧的渲染耗时分布。
关键注意事项与避坑指南
- 线程限制:
Choreographer与 Looper 绑定,主线程的Choreographer负责 UI 渲染,子线程的Choreographer无法操作 UI,且只有拥有 Looper 的线程才能获取Choreographer实例。 - 避免频繁提交任务:向
Choreographer提交过多任务(尤其是TRAVERSAL队列),会导致单个 VSYNC 周期内任务无法完成,引发掉帧卡顿。 - 帧回调的取消:持续监控帧率时,若无需再监控,需调用
removeFrameCallback()取消回调,否则会导致内存泄漏与不必要的 CPU 消耗:
// 取消帧回调监控
choreographer.removeFrameCallback(frameCallbackInstance)
- 卡顿的核心原因:
Choreographer仅负责调度任务,若measure、layout、draw等任务耗时过长(超过 VSYNC 周期),或主线程被 I/O、复杂计算阻塞,即使Choreographer调度正常,也会出现掉帧卡顿。 - 高刷新率屏幕适配:120Hz、144Hz 高刷新率屏幕的 VSYNC 周期更短(8.33ms 左右),对任务耗时的要求更高,需通过
Choreographer监控适配,避免高刷新率下的卡顿。
VSYNC(屏幕垂直同步信号)
屏幕的垂直同步信号(Vertical Sync,简称 VSYNC) 是硬件层面由屏幕显示控制器发出的周期性同步触发信号,是协调 CPU/GPU 渲染节奏与屏幕刷新节奏的核心机制,直接决定了 UI 渲染的流畅性。
一、 VSYNC 的核心背景:屏幕显示原理
要理解 VSYNC,首先要知道屏幕的逐帧刷新机制:
屏幕的刷新方式:无论是手机、电脑显示器,本质都是
逐行扫描像素
来显示一帧画面。
- 对 LCD/OLED 屏幕,显示控制器会从屏幕左上角开始,逐行向像素发送显示数据,直到右下角,完成一帧画面的扫描。
- 扫描完成后,显示控制器会回到左上角,准备扫描下一帧,这个「回到起点」的过程称为 垂直消隐期(Vertical Blank Interval, VBI)。
刷新率(Refresh Rate):屏幕每秒能完成的「整帧扫描次数」就是刷新率,单位是 Hz。
- 常见的屏幕刷新率:60Hz(每 16.67ms 刷新一次)、90Hz(11.11ms)、120Hz(8.33ms)、144Hz(6.94ms)。
- 刷新率是屏幕的硬件属性,决定了 VSYNC 信号的周期。
二、 VSYNC 信号的本质与作用
1. 信号本质
VSYNC 信号是在每帧扫描完成、进入垂直消隐期时,由显示控制器发出的一个硬件中断信号。
- 触发时机:刚好在「上一帧显示完成,下一帧准备开始」的临界点。
- 信号周期:等于屏幕的帧间隔时间,例如 60Hz 屏幕的 VSYNC 周期是
1000ms / 60 ≈ 16.67ms。
2. 核心作用:解决「撕裂」与「浪费」
没有 VSYNC 时,CPU/GPU 渲染完一帧数据就直接提交给屏幕,会导致两个严重问题:
| 问题 | 现象 | 危害 |
|---|---|---|
| 画面撕裂 | 屏幕正在显示上一帧的下半部分时,GPU 提交了新帧的上半部分,导致屏幕上下两部分显示不同帧的内容,出现明显的「分割线」 | 严重影响视觉体验,动画 / 滑动时尤为明显 |
| 资源浪费 | CPU/GPU 渲染速度远超屏幕刷新率(如 GPU 10ms 渲染一帧,屏幕 16.67ms 刷新一次),多余的渲染帧会被丢弃 | 白白消耗 CPU/GPU 资源,增加功耗 |
VSYNC 信号的核心价值就是强制 CPU/GPU 的渲染节奏与屏幕刷新节奏对齐:
- 只有收到 VSYNC 信号,CPU/GPU 才会开始渲染下一帧数据。
- 渲染完成后,刚好赶上屏幕的下一次刷新,直接显示新帧,既避免撕裂,又减少资源浪费。
三、 VSYNC 在 Android 系统中的工作流程
在 Android 中,VSYNC 信号不仅是屏幕的硬件信号,还会通过系统服务传递给应用层,成为 Choreographer 调度 UI 渲染的核心触发源,完整流程如下:
硬件层触发 VSYNC显示控制器完成一帧扫描,进入垂直消隐期,发出 VSYNC 硬件信号。
系统层分发信号Android 系统的 SurfaceFlinger(负责屏幕合成的核心服务)接收 VSYNC 信号,一方面触发屏幕刷新新帧,另一方面通过 DisplayEventReceiver 将信号分发到应用进程。
应用层响应信号
应用进程的
Choreographer接收到 VSYNC 信号后,按优先级执行四大任务队列(INPUT → ANIMATION → TRAVERSAL → COMMIT),其中TRAVERSAL队列会触发 View 树的measure/layout/draw流程,生成新的帧数据。提交渲染数据
应用将渲染好的帧数据提交给 SurfaceFlinger,SurfaceFlinger 合成后,等待下一个 VSYNC 信号到来时,将新帧显示到屏幕上。
关键结论:Android 应用的 UI 渲染不是主动触发的,而是被动等待 VSYNC 信号触发的,Choreographer 就是这个等待机制的「调度者」。
四、 VSYNC 与流畅性的关系
UI 流畅的核心标准是 「每帧渲染耗时 ≤ VSYNC 周期」:
- 对 60Hz 屏幕,每帧渲染必须在 16.67ms 内完成,否则会掉帧(错过当前 VSYNC 周期,只能等到下一个周期显示,相当于 2 个周期显示 1 帧,帧率降到 30Hz)。
- 对 120Hz 屏幕,每帧渲染需在 8.33ms 内完成,对性能要求更高,但流畅度也更好。
举个例子:
- 正常情况:60Hz 屏幕,每 16.67ms 收到一次 VSYNC,CPU/GPU 10ms 完成渲染,准时显示 → 帧率 60Hz,流畅。
- 卡顿情况:某一帧因为主线程阻塞(如执行耗时 I/O),渲染耗时 20ms → 错过本次 VSYNC,只能等下一次 → 这 33.34ms 只显示 1 帧,帧率降到 30Hz → 视觉上出现卡顿。
五、 关键补充:「垂直同步」开关的含义
很多游戏或视频应用会有「垂直同步」开关,其本质是:
- 开启:强制 GPU 渲染节奏与 VSYNC 同步,避免画面撕裂,但如果渲染速度跟不上刷新率,会导致帧率被限制在刷新率(如 60Hz)。
- 关闭:GPU 渲染不受 VSYNC 限制,渲染速度快于刷新率时会出现画面撕裂,但帧率可能超过屏幕刷新率(适合追求极限帧率的场景)。
而 Android 应用的 UI 渲染默认是强制开启 VSYNC 同步的,无法手动关闭,这是保证系统 UI 流畅性的基础。
获取屏幕的垂直同步信号?
在 Kotlin 中,无法直接获取硬件层面的 VSYNC 原始信号(该信号由屏幕控制器发出,仅系统服务 SurfaceFlinger 可直接接收),但开发者可以通过 Android 提供的 Choreographer 类,监听 VSYNC 信号触发的帧回调(间接响应 VSYNC 信号),这也是应用层对接 VSYNC 最核心、最常用的方式(所有帧率监控、卡顿排查工具均基于此实现)。
以下是详细的实现方案、代码示例和注意事项:
一、 核心原理
Choreographer会通过系统底层的DisplayEventReceiver注册 VSYNC 监听,接收来自SurfaceFlinger分发的 VSYNC 信号(已转化为应用层可识别的事件)。- 当 VSYNC 信号到达时,
Choreographer会触发FrameCallback回调接口的doFrame()方法,同时传递当前帧的 VSYNC 时间戳。 - 我们通过实现
FrameCallback接口,即可在应用层「感知」到 VSYNC 信号的到来(间接获取 VSYNC 触发事件)。
二、 完整 Kotlin 实现代码
方案 1:基础监听(单次 / 持续响应 VSYNC 信号)
适合监控帧率、感知每帧渲染时机,是最常用的场景。
import android.os.Bundle
import android.view.Choreographer
import androidx.appcompat.app.AppCompatActivity
import java.util.concurrent.TimeUnit
class VSyncMonitorActivity : AppCompatActivity() {
// 1. 声明 Choreographer 实例和 FrameCallback 实例(便于后续取消监听)
private lateinit var choreographer: Choreographer
private val frameCallback = object : Choreographer.FrameCallback {
// 上一帧 VSYNC 时间戳(纳秒),用于计算帧间隔
private var lastVsyncTimeNanos: Long = 0
/**
* VSYNC 信号到达时触发此方法(每帧回调一次)
* @param frameTimeNanos 当前帧 VSYNC 信号的时间戳(纳秒),对应 VSYNC 信号触发时刻
*/
override fun doFrame(frameTimeNanos: Long) {
// 处理 VSYNC 信号(此处可执行与帧同步的任务)
handleVsyncSignal(frameTimeNanos)
// 【关键】若需要持续监听 VSYNC 信号,需重新注册回调(单次回调仅触发一次)
// 取消持续监听:注释此行,或调用 choreographer.removeFrameCallback(this)
choreographer.postFrameCallback(this)
}
/**
* 处理 VSYNC 信号(解析时间戳、计算帧间隔/帧率)
*/
private fun handleVsyncSignal(currentVsyncNanos: Long) {
if (lastVsyncTimeNanos != 0L) {
// 1. 计算两帧 VSYNC 信号的间隔(毫秒),即帧耗时
val frameIntervalMs = TimeUnit.NANOSECONDS.toMillis(currentVsyncNanos - lastVsyncTimeNanos)
// 2. 计算当前帧率(理论值,实际场景需平滑处理)
val frameRate = if (frameIntervalMs > 0) 1000.0 / frameIntervalMs else 0.0
// 3. 打印 VSYNC 相关信息(间接获取到 VSYNC 信号的触发结果)
println("VSYNC 信号触发:帧间隔 = $frameIntervalMs ms,帧率 = %.2f fps".format(frameRate))
}
// 更新上一帧 VSYNC 时间戳
lastVsyncTimeNanos = currentVsyncNanos
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 2. 获取主线程对应的 Choreographer 实例(UI 渲染的 VSYNC 信号对应主线程实例)
choreographer = Choreographer.getInstance()
}
override fun onResume() {
super.onResume()
// 3. 注册 FrameCallback,开始监听 VSYNC 信号(页面可见时开启)
choreographer.postFrameCallback(frameCallback)
}
override fun onPause() {
super.onPause()
// 4. 取消 FrameCallback,停止监听 VSYNC 信号(页面不可见时关闭,避免资源浪费)
choreographer.removeFrameCallback(frameCallback)
}
}
方案 2:提交自定义任务到 VSYNC 同步队列
若需要让自定义任务与 VSYNC 信号同步执行(如动画优化、自定义 View 刷新),可通过 Choreographer.postCallback() 提交任务,任务会在下次 VSYNC 信号到来时执行。
// 在 Activity/Fragment 或自定义 View 中调用
private fun postTaskWithVsync() {
val choreographer = Choreographer.getInstance()
// 提交任务到 ANIMATION 队列(与动画任务同步,优先级仅次于输入事件)
choreographer.postCallback(
Choreographer.CALLBACK_ANIMATION, // 任务队列类型(对应 VSYNC 调度的任务优先级)
Runnable {
// 自定义任务:与 VSYNC 信号同步执行,保证任务与帧渲染时序对齐
println("自定义任务与 VSYNC 信号同步执行")
},
null // 额外参数,可传递任务相关数据(无需时传 null)
)
}
三、 关键说明
无法获取原始硬件 VSYNC 信号
应用层没有权限直接访问硬件层面的 VSYNC 信号,只能通过
Choreographer间接感知其触发事件,frameTimeNanos是 VSYNC 信号到达应用进程的时间戳,并非硬件发出的原始时间戳。Choreographer与线程绑定- 调用
Choreographer.getInstance()会获取当前线程对应的Choreographer实例,主线程的实例对应 UI 渲染的 VSYNC 信号,子线程实例(需线程拥有Looper)无法感知 UI 相关的 VSYNC 信号。 - UI 相关的 VSYNC 监听必须在主线程执行,子线程无法触发 UI 帧回调。
- 调用
持续监听与资源释放
- 持续监听需在
doFrame()中重新调用postFrameCallback(this),形成循环。 - 页面销毁 / 无需监听时,必须调用
removeFrameCallback()取消监听,否则会导致内存泄漏和不必要的 CPU 消耗。
- 持续监听需在
VSYNC 周期与屏幕刷新率
- 回调的间隔时间等于屏幕的 VSYNC 周期(如 60Hz 屏幕≈16.67ms,120Hz 屏幕≈8.33ms)。
- 若某一帧渲染耗时超过 VSYNC 周期,会跳过一次回调,间隔时间变为 2 倍 VSYNC 周期(如 33.34ms),这也是掉帧卡顿的核心特征。
四、 进阶:封装 VSYNC 监听工具类
便于在项目中复用,支持开启 / 关闭监听、回调结果分发:
import android.view.Choreographer
import java.util.concurrent.TimeUnit
class VSyncMonitor private constructor() {
private lateinit var choreographer: Choreographer
private var lastVsyncTimeNanos: Long = 0
private var onVsyncListener: OnVsyncListener? = null
private val frameCallback = Choreographer.FrameCallback { currentNanos ->
onVsyncListener?.let { listener ->
if (lastVsyncTimeNanos != 0L) {
val frameIntervalMs = TimeUnit.NANOSECONDS.toMillis(currentNanos - lastVsyncTimeNanos)
val frameRate = 1000.0 / frameIntervalMs
// 分发 VSYNC 回调结果
listener.onVsyncTriggered(frameIntervalMs, frameRate)
}
lastVsyncTimeNanos = currentNanos
}
// 持续监听
if (onVsyncListener != null) {
choreographer.postFrameCallback(frameCallback)
}
}
/**
* 开启 VSYNC 监听
*/
fun startListening(listener: OnVsyncListener) {
choreographer = Choreographer.getInstance()
onVsyncListener = listener
choreographer.postFrameCallback(frameCallback)
}
/**
* 停止 VSYNC 监听
*/
fun stopListening() {
choreographer.removeFrameCallback(frameCallback)
onVsyncListener = null
lastVsyncTimeNanos = 0
}
/**
* VSYNC 监听回调接口
*/
interface OnVsyncListener {
/**
* VSYNC 信号触发回调
* @param frameIntervalMs 帧间隔时间(毫秒)
* @param frameRate 当前帧率(fps)
*/
fun onVsyncTriggered(frameIntervalMs: Long, frameRate: Double)
}
companion object {
val instance: VSyncMonitor by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
VSyncMonitor()
}
}
}
工具类使用示例
// 开启 VSYNC 监听
VSyncMonitor.instance.startListening(object : VSyncMonitor.OnVsyncListener {
override fun onVsyncTriggered(frameIntervalMs: Long, frameRate: Double) {
println("VSYNC 回调:帧间隔 = $frameIntervalMs ms,帧率 = %.2f fps".format(frameRate))
}
})
// 停止 VSYNC 监听(如页面销毁时)
VSyncMonitor.instance.stopListening()
五、 总结
- 应用层无法直接获取硬件 VSYNC 信号,需通过
Choreographer.FrameCallback间接监听。 - 核心 API:
Choreographer.getInstance()获取实例,postFrameCallback()注册监听,removeFrameCallback()取消监听。 doFrame()方法的frameTimeNanos参数对应 VSYNC 信号触发时间,可用于计算帧间隔和帧率。- 持续监听需循环注册回调,且必须在无需使用时取消,避免内存泄漏和资源浪费。
Android 中 Choreographer 完整使用指南
Choreographer 是 Android 系统 UI 渲染的核心调度类,负责协调 UI 任务与 VSYNC 信号同步,在应用层的核心使用场景是监听帧渲染、监控帧率 / 卡顿、同步自定义任务与 UI 渲染时序。
Choreographer是 Android UI 渲染的调度核心,应用层无法直接操作,需通过其提供的 API 间接对接 VSYNC 信号。- 核心 API:
getInstance()获取实例,postFrameCallback()/removeFrameCallback()监听 / 取消帧渲染,postCallback()提交自定义任务。 - 核心场景:监控帧率 / 卡顿、同步自定义任务与 UI 渲染、优化自定义 View 绘制。
- 避坑关键:主线程执行、及时释放回调、避免高优先级队列提交耗时任务。
以下是其完整使用方法、代码示例和最佳实践。
一、 Choreographer 核心基础
1. 核心获取方式
Choreographer 采用单例模式,与线程 Looper 绑定(每个拥有 Looper 的线程对应一个实例),UI 相关操作需获取主线程实例,通过静态方法 getInstance() 获取:
// 获取当前线程对应的 Choreographer 实例(主线程调用即为 UI 渲染核心实例)
val choreographer = Choreographer.getInstance()
注意:子线程获取
Choreographer实例前,需先初始化Looper(如Looper.prepare()),否则会抛出异常,且子线程实例无法操作 UI。
2. 核心使用场景
- 监听帧回调,监控帧率、排查 UI 卡顿(最常用)。
- 提交自定义任务到指定优先级队列,与 UI 渲染时序对齐。
- 自定义 View 渲染优化,避免频繁刷新导致过度绘制。
二、 场景 1:监听帧回调(监控帧率 / 卡顿)
这是 Choreographer 最核心的应用,通过 FrameCallback 接口监听每帧渲染(对应 VSYNC 信号触发),可计算帧间隔、帧率,判定卡顿。
完整实现代码
import android.os.Bundle
import android.view.Choreographer
import androidx.appcompat.app.AppCompatActivity
import java.util.concurrent.TimeUnit
class ChoreographerFrameMonitorActivity : AppCompatActivity() {
// 1. 声明 Choreographer 与 FrameCallback 实例(便于后续取消监听)
private lateinit var choreographer: Choreographer
private var lastFrameTimeNanos: Long = 0 // 上一帧 VSYNC 时间戳(纳秒)
private val frameCallback = object : Choreographer.FrameCallback {
override fun doFrame(currentFrameTimeNanos: Long) {
// 此方法在 VSYNC 信号触发时调用,对应每帧渲染的开始
analyzeFramePerformance(currentFrameTimeNanos)
// 【关键】持续监听:重新注册回调(单次回调可注释此行)
choreographer.postFrameCallback(this)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 2. 获取主线程 Choreographer 实例
choreographer = Choreographer.getInstance()
}
override fun onResume() {
super.onResume()
// 3. 开启帧监听(页面可见时启动)
startFrameMonitor()
}
override fun onPause() {
super.onPause()
// 4. 停止帧监听(页面不可见时关闭,避免资源浪费)
stopFrameMonitor()
}
/**
* 开启帧性能监控
*/
private fun startFrameMonitor() {
lastFrameTimeNanos = 0
// 注册帧回调,下次 VSYNC 信号触发时执行 doFrame()
choreographer.postFrameCallback(frameCallback)
}
/**
* 停止帧性能监控
*/
private fun stopFrameMonitor() {
// 移除帧回调,终止监听
choreographer.removeFrameCallback(frameCallback)
}
/**
* 分析帧性能:计算帧间隔、帧率,判定卡顿
*/
private fun analyzeFramePerformance(currentFrameTimeNanos: Long) {
if (lastFrameTimeNanos == 0L) {
// 第一帧仅初始化时间戳,不做计算
lastFrameTimeNanos = currentFrameTimeNanos
return
}
// 1. 计算帧间隔(两帧 VSYNC 信号的时间差,单位:毫秒)
val frameIntervalMs = TimeUnit.NANOSECONDS.toMillis(currentFrameTimeNanos - lastFrameTimeNanos)
// 2. 计算当前帧率(fps,每秒渲染帧数)
val frameRate = if (frameIntervalMs > 0) 1000.0 / frameIntervalMs else 0.0
// 3. 判定卡顿:60Hz 屏幕帧间隔 > 16.67ms,120Hz 屏幕 > 8.33ms
val isJank = frameIntervalMs > 16.67 // 兼容主流 60Hz 屏幕,高刷可调整阈值
// 4. 打印性能日志
val logMsg = "帧间隔:%.2f ms | 帧率:%.1f fps | 卡顿:%b".format(
frameIntervalMs, frameRate, isJank
)
if (isJank) {
android.util.Log.w("ChoreographerDemo", "【卡顿预警】$logMsg")
} else {
android.util.Log.d("ChoreographerDemo", logMsg)
}
// 更新上一帧时间戳
lastFrameTimeNanos = currentFrameTimeNanos
}
}
关键说明
doFrame(currentFrameTimeNanos: Long):currentFrameTimeNanos是当前帧 VSYNC 信号的时间戳(纳秒),对应帧渲染的起始时刻。- 持续监听:需在
doFrame()中重新调用postFrameCallback(this),形成循环;单次监听可省略此行,仅响应一次 VSYNC 信号。 - 资源释放:页面不可见时必须调用
removeFrameCallback(),避免内存泄漏和不必要的 CPU 消耗。 - 卡顿判定:核心标准是「帧间隔 > 屏幕 VSYNC 周期」(60Hz 对应 16.67ms,120Hz 对应 8.33ms)。
三、 场景 2:提交自定义任务到指定队列
Choreographer 维护了 4 个优先级任务队列,可通过 postCallback() 将自定义任务提交到指定队列,确保任务与 UI 渲染时序对齐,优化任务执行效率。
4 个任务队列(优先级从高到低)
| 队列常量 | 优先级 | 核心用途 |
|---|---|---|
Choreographer.CALLBACK_INPUT | 最高 | 处理用户输入事件(如触摸、按键),自定义任务尽量不占用此队列 |
Choreographer.CALLBACK_ANIMATION | 次高 | 执行动画相关任务(如 ValueAnimator),保证动画流畅性 |
Choreographer.CALLBACK_TRAVERSAL | 中等 | 执行 View 测量、布局、绘制任务,自定义 UI 刷新任务可提交至此 |
Choreographer.CALLBACK_COMMIT | 最低 | 提交渲染数据、清理资源,后续收尾任务可提交至此 |
完整实现代码
import android.os.Bundle
import android.view.Choreographer
import androidx.appcompat.app.AppCompatActivity
class ChoreographerTaskSubmitActivity : AppCompatActivity() {
private lateinit var choreographer: Choreographer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
choreographer = Choreographer.getInstance()
// 提交自定义任务到不同队列
submitTaskToAnimationQueue()
submitTaskToTraversalQueue()
}
/**
* 提交任务到 ANIMATION 队列(与动画同步执行)
*/
private fun submitTaskToAnimationQueue() {
choreographer.postCallback(
Choreographer.CALLBACK_ANIMATION, // 队列类型
Runnable {
// 自定义动画相关任务:如更新动画参数、刷新自定义 View 动画状态
android.util.Log.d("ChoreographerDemo", "执行动画队列任务:与 VSYNC 同步执行")
},
null // 额外参数(无需时传 null,可通过 `Choreographer.removeCallbacks()` 移除)
)
}
/**
* 提交任务到 TRAVERSAL 队列(与 View 渲染同步执行)
*/
private fun submitTaskToTraversalQueue() {
choreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL,
Runnable {
// 自定义 UI 相关任务:如更新 View 数据、触发轻量刷新
android.util.Log.d("ChoreographerDemo", "执行遍历队列任务:与 View measure/layout/draw 同步")
},
null
)
}
/**
* 可选:提交延迟执行的任务(与 VSYNC 信号对齐,延迟指定时间)
*/
private fun submitDelayedTask() {
choreographer.postCallbackDelayed(
Choreographer.CALLBACK_COMMIT,
Runnable {
android.util.Log.d("ChoreographerDemo", "执行延迟任务:延迟 1000ms 且与 VSYNC 同步")
},
null,
1000 // 延迟时间(毫秒)
)
}
}
关键说明
- 任务执行时机:提交的任务会在下一次 VSYNC 信号触发时,按队列优先级执行,避免与核心 UI 任务竞争资源。
- 延迟任务:
postCallbackDelayed()支持延迟提交任务,延迟时间到后,需等待下一个 VSYNC 信号才会执行,保证与帧渲染对齐。 - 任务移除:可通过
choreographer.removeCallbacks(queueType, runnable, token)移除未执行的任务,避免无用任务执行。
四、 场景 3:自定义 View 渲染优化
在自定义 View 中,使用 Choreographer 调度刷新任务,替代频繁调用 invalidate(),避免过度绘制,优化渲染性能。
完整实现代码(自定义 View)
import android.content.Context
import android.graphics.Canvas
import android.view.Choreographer
import android.view.View
class OptimizedCustomView(context: Context) : View(context), Choreographer.FrameCallback {
private val choreographer = Choreographer.getInstance()
private var progress: Float = 0f // 自定义渲染进度
/**
* 触发自定义 View 刷新(替代直接调用 invalidate())
*/
fun refreshView() {
// 注册帧回调,下一次 VSYNC 信号触发时执行 doFrame(),再触发绘制
choreographer.postFrameCallback(this)
}
override fun doFrame(frameTimeNanos: Long) {
// 1. 更新渲染数据
progress += 0.01f
if (progress > 1f) progress = 0f
// 2. 触发 View 绘制(仅执行一次绘制,避免频繁刷新)
invalidate()
// 3. 如需持续刷新(如动画),重新注册回调;否则注释此行
choreographer.postFrameCallback(this)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 4. 执行自定义绘制逻辑(使用 progress 变量)
canvas.drawText("渲染进度:%.2f".format(progress), 50f, 100f, paint)
}
// 简化:初始化画笔
private val paint by lazy {
android.graphics.Paint().apply {
textSize = 30f
color = android.graphics.Color.BLACK
}
}
/**
* 销毁 View 时释放资源
*/
fun onViewDestroy() {
choreographer.removeFrameCallback(this)
}
}
关键优势
- 避免过度绘制:
invalidate()会触发立即刷新,可能导致同一 VSYNC 周期内多次绘制;而Choreographer调度的绘制,每 VSYNC 周期仅执行一次。 - 与 VSYNC 对齐:绘制任务与系统 UI 渲染时序一致,减少 CPU/GPU 资源浪费,提升流畅性。
五、 关键注意事项与避坑指南
线程限制
- 主线程的
Choreographer负责 UI 渲染,仅能在主线程执行 UI 相关操作(如invalidate()、提交TRAVERSAL队列任务)。 - 子线程的
Choreographer无法操作 UI,仅可用于监听子线程内部的任务调度,无实际 UI 优化价值。
- 主线程的
避免内存泄漏
FrameCallback若持有Activity/View强引用,需在页面销毁 / View 回收时,调用removeFrameCallback()取消监听。建议使用
WeakReference持有外部引用(如 Activity),避免内存泄漏:class WeakFrameCallback(activity: ChoreographerFrameMonitorActivity) : Choreographer.FrameCallback { private val activityRef = WeakReference(activity) override fun doFrame(frameTimeNanos: Long) { val activity = activityRef.get() ?: return // 引用已回收,直接返回 // 执行后续逻辑 } }
不依赖帧回调时间戳做精准计时
currentFrameTimeNanos是 VSYNC 信号到达应用进程的时间戳,并非硬件发出的原始时间戳,存在微小延迟,不可用于高精度计时场景。
避免在高优先级队列提交耗时任务
INPUT和ANIMATION队列优先级高,若提交耗时任务(如复杂计算、I/O),会阻塞用户输入和动画执行,导致卡顿。