rokevin
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
  • Choreographer

  • Choreographer 核心定位与作用
    • 1. 核心定位
    • 2. 核心作用
  • Choreographer 核心工作原理
    • 1. 核心依赖:VSYNC 信号
    • 2. 任务队列:四大类型有序执行
    • 3. 核心工作流程(简化版)
  • 关键 API 与实际应用
    • 1. 核心 API (开发者常用)
      • (1) 获取 Choreographer 实例
      • (2) 监听帧回调(排查卡顿、监控帧率)
      • (3) 提交任务到指定队列
    • 2. 实际应用场景
  • 关键注意事项与避坑指南
  • VSYNC(屏幕垂直同步信号)
    • 一、 VSYNC 的核心背景:屏幕显示原理
    • 二、 VSYNC 信号的本质与作用
    • 1. 信号本质
    • 2. 核心作用:解决「撕裂」与「浪费」
    • 三、 VSYNC 在 Android 系统中的工作流程
    • 四、 VSYNC 与流畅性的关系
    • 五、 关键补充:「垂直同步」开关的含义
  • 获取屏幕的垂直同步信号?
    • 一、 核心原理
    • 二、 完整 Kotlin 实现代码
      • 方案 1:基础监听(单次 / 持续响应 VSYNC 信号)
      • 方案 2:提交自定义任务到 VSYNC 同步队列
    • 三、 关键说明
    • 四、 进阶:封装 VSYNC 监听工具类
      • 工具类使用示例
    • 五、 总结
  • Android 中 Choreographer 完整使用指南
    • 一、 Choreographer 核心基础
      • 1. 核心获取方式
      • 2. 核心使用场景
    • 二、 场景 1:监听帧回调(监控帧率 / 卡顿)
      • 完整实现代码
      • 关键说明
    • 三、 场景 2:提交自定义任务到指定队列
      • 4 个任务队列(优先级从高到低)
      • 完整实现代码
      • 关键说明
    • 四、 场景 3:自定义 View 渲染优化
      • 完整实现代码(自定义 View)
      • 关键优势
    • 五、 关键注意事项与避坑指南

Choreographer

Choreographer是 Android 系统 UI 渲染流程的核心调度者,负责协调 UI 渲染的各个环节,保证页面绘制与屏幕刷新率同步,是解决 UI 卡顿、掉帧问题的关键核心类。

  1. Choreographer 是 Android UI 渲染的「编舞者」,核心作用是协调 UI 任务与 VSYNC 信号同步,保证 UI 流畅性。
  2. 内部维护 4 个优先级任务队列(INPUT > ANIMATION > TRAVERSAL > COMMIT),按序执行确保核心任务优先完成。
  3. 常用 FrameCallback 监听帧渲染,用于帧率监控与卡顿排查,是性能优化工具的核心基础。
  4. 卡顿的本质是「单个 VSYNC 周期内任务无法完成」,Choreographer 可帮助定位问题,但优化需聚焦于减少 measure/layout/draw 耗时与主线程阻塞。

Choreographer 核心定位与作用

1. 核心定位

Choreographer 直译是「编舞者」,位于 android.view 包下,是 Android 系统用于协调 UI 渲染、输入事件、动画执行的核心调度类,本质是一个「同步调度器」,将各类与 UI 相关的任务与屏幕的垂直同步信号(VSYNC)对齐。

2. 核心作用

  1. 接收 VSYNC 信号:监听屏幕硬件发出的垂直同步信号(VSYNC,Vertical Sync),该信号由屏幕刷新率决定(如 60Hz 屏幕每 16.67ms 发送一次,120Hz 屏幕每 8.33ms 发送一次)。
  2. 调度 UI 渲染任务:协调 Measure(测量)、Layout(布局)、Draw(绘制)、Commit(合成)等 UI 渲染环节,确保每一轮渲染任务都在一个 VSYNC 周期内完成,避免掉帧。
  3. 统一任务调度:将输入事件(如触摸、按键)、动画执行(如 ValueAnimator)、Traversal 任务(UI 刷新)统一纳入调度,保证任务执行的时序一致性,避免竞争资源导致的卡顿。
  4. 避免过度绘制与卡顿:通过与 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. 核心工作流程(简化版)

  1. 任务入队:当应用触发 UI 刷新(如 View.invalidate()、Activity.onResume())、动画启动、用户输入时,相关任务会被加入对应优先级的任务队列。
  2. 注册 VSYNC 监听:如果当前没有注册 VSYNC 监听,Choreographer 会通过 DisplayEventReceiver 向系统注册下一个 VSYNC 信号监听。
  3. 接收 VSYNC 信号:屏幕发出 VSYNC 信号后,DisplayEventReceiver 回调 onVsync() 方法,通知 Choreographer 开始执行任务。
  4. 按优先级执行任务:Choreographer 依次执行 INPUT → ANIMATION → TRAVERSAL → COMMIT 队列中的任务,其中 TRAVERSAL 队列的任务会触发 ViewRootImpl.performTraversals(),进而执行 View 树的 measure、layout、draw。
  5. 提交渲染数据:任务执行完成后,将绘制好的帧数据提交给 GPU,由 GPU 合成后通过 SurfaceFlinger 显示到屏幕上。
  6. 循环等待下一个 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. 实际应用场景

  1. 帧率监控与卡顿排查:如 BlockCanary、Trace Canary 等工具,通过 FrameCallback 监听每帧耗时,定位主线程阻塞导致的掉帧。
  2. 动画流畅性优化:将动画相关任务提交到 ANIMATION 队列,确保动画与 VSYNC 信号同步,避免动画抖动。
  3. 自定义 View 渲染优化:在自定义 View 中,通过 Choreographer 调度刷新任务,避免频繁调用 invalidate() 导致过度绘制。
  4. 性能测试工具:如 Android Studio Frame Profiler,通过监听 Choreographer 的任务执行,分析每帧的渲染耗时分布。

关键注意事项与避坑指南

  1. 线程限制:Choreographer 与 Looper 绑定,主线程的 Choreographer 负责 UI 渲染,子线程的 Choreographer 无法操作 UI,且只有拥有 Looper 的线程才能获取 Choreographer 实例。
  2. 避免频繁提交任务:向 Choreographer 提交过多任务(尤其是 TRAVERSAL 队列),会导致单个 VSYNC 周期内任务无法完成,引发掉帧卡顿。
  3. 帧回调的取消:持续监控帧率时,若无需再监控,需调用 removeFrameCallback() 取消回调,否则会导致内存泄漏与不必要的 CPU 消耗:
// 取消帧回调监控
choreographer.removeFrameCallback(frameCallbackInstance)
  1. 卡顿的核心原因:Choreographer 仅负责调度任务,若 measure、layout、draw 等任务耗时过长(超过 VSYNC 周期),或主线程被 I/O、复杂计算阻塞,即使 Choreographer 调度正常,也会出现掉帧卡顿。
  2. 高刷新率屏幕适配:120Hz、144Hz 高刷新率屏幕的 VSYNC 周期更短(8.33ms 左右),对任务耗时的要求更高,需通过 Choreographer 监控适配,避免高刷新率下的卡顿。

VSYNC(屏幕垂直同步信号)

屏幕的垂直同步信号(Vertical Sync,简称 VSYNC) 是硬件层面由屏幕显示控制器发出的周期性同步触发信号,是协调 CPU/GPU 渲染节奏与屏幕刷新节奏的核心机制,直接决定了 UI 渲染的流畅性。

一、 VSYNC 的核心背景:屏幕显示原理

要理解 VSYNC,首先要知道屏幕的逐帧刷新机制:

  1. 屏幕的刷新方式:无论是手机、电脑显示器,本质都是

    逐行扫描像素

    来显示一帧画面。

    • 对 LCD/OLED 屏幕,显示控制器会从屏幕左上角开始,逐行向像素发送显示数据,直到右下角,完成一帧画面的扫描。
    • 扫描完成后,显示控制器会回到左上角,准备扫描下一帧,这个「回到起点」的过程称为 垂直消隐期(Vertical Blank Interval, VBI)。
  2. 刷新率(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 渲染的核心触发源,完整流程如下:

  1. 硬件层触发 VSYNC显示控制器完成一帧扫描,进入垂直消隐期,发出 VSYNC 硬件信号。

  2. 系统层分发信号Android 系统的 SurfaceFlinger(负责屏幕合成的核心服务)接收 VSYNC 信号,一方面触发屏幕刷新新帧,另一方面通过 DisplayEventReceiver 将信号分发到应用进程。

  3. 应用层响应信号

    应用进程的 Choreographer 接收到 VSYNC 信号后,按优先级执行四大任务队列(INPUT → ANIMATION → TRAVERSAL → COMMIT),其中 TRAVERSAL 队列会触发 View 树的 measure/layout/draw 流程,生成新的帧数据。

  4. 提交渲染数据

    应用将渲染好的帧数据提交给 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 最核心、最常用的方式(所有帧率监控、卡顿排查工具均基于此实现)。

以下是详细的实现方案、代码示例和注意事项:

一、 核心原理

  1. Choreographer 会通过系统底层的 DisplayEventReceiver 注册 VSYNC 监听,接收来自 SurfaceFlinger 分发的 VSYNC 信号(已转化为应用层可识别的事件)。
  2. 当 VSYNC 信号到达时,Choreographer 会触发 FrameCallback 回调接口的 doFrame() 方法,同时传递当前帧的 VSYNC 时间戳。
  3. 我们通过实现 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)
    )
}

三、 关键说明

  1. 无法获取原始硬件 VSYNC 信号

    应用层没有权限直接访问硬件层面的 VSYNC 信号,只能通过 Choreographer 间接感知其触发事件,frameTimeNanos 是 VSYNC 信号到达应用进程的时间戳,并非硬件发出的原始时间戳。

  2. Choreographer 与线程绑定

    • 调用 Choreographer.getInstance() 会获取当前线程对应的 Choreographer 实例,主线程的实例对应 UI 渲染的 VSYNC 信号,子线程实例(需线程拥有 Looper)无法感知 UI 相关的 VSYNC 信号。
    • UI 相关的 VSYNC 监听必须在主线程执行,子线程无法触发 UI 帧回调。
  3. 持续监听与资源释放

    • 持续监听需在 doFrame() 中重新调用 postFrameCallback(this),形成循环。
    • 页面销毁 / 无需监听时,必须调用 removeFrameCallback() 取消监听,否则会导致内存泄漏和不必要的 CPU 消耗。
  4. 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()

五、 总结

  1. 应用层无法直接获取硬件 VSYNC 信号,需通过 Choreographer.FrameCallback 间接监听。
  2. 核心 API:Choreographer.getInstance() 获取实例,postFrameCallback() 注册监听,removeFrameCallback() 取消监听。
  3. doFrame() 方法的 frameTimeNanos 参数对应 VSYNC 信号触发时间,可用于计算帧间隔和帧率。
  4. 持续监听需循环注册回调,且必须在无需使用时取消,避免内存泄漏和资源浪费。

Android 中 Choreographer 完整使用指南

Choreographer 是 Android 系统 UI 渲染的核心调度类,负责协调 UI 任务与 VSYNC 信号同步,在应用层的核心使用场景是监听帧渲染、监控帧率 / 卡顿、同步自定义任务与 UI 渲染时序。

  1. Choreographer 是 Android UI 渲染的调度核心,应用层无法直接操作,需通过其提供的 API 间接对接 VSYNC 信号。
  2. 核心 API:getInstance() 获取实例,postFrameCallback()/removeFrameCallback() 监听 / 取消帧渲染,postCallback() 提交自定义任务。
  3. 核心场景:监控帧率 / 卡顿、同步自定义任务与 UI 渲染、优化自定义 View 绘制。
  4. 避坑关键:主线程执行、及时释放回调、避免高优先级队列提交耗时任务。

以下是其完整使用方法、代码示例和最佳实践。

一、 Choreographer 核心基础

1. 核心获取方式

Choreographer 采用单例模式,与线程 Looper 绑定(每个拥有 Looper 的线程对应一个实例),UI 相关操作需获取主线程实例,通过静态方法 getInstance() 获取:

// 获取当前线程对应的 Choreographer 实例(主线程调用即为 UI 渲染核心实例)
val choreographer = Choreographer.getInstance()

注意:子线程获取 Choreographer 实例前,需先初始化 Looper(如 Looper.prepare()),否则会抛出异常,且子线程实例无法操作 UI。

2. 核心使用场景

  1. 监听帧回调,监控帧率、排查 UI 卡顿(最常用)。
  2. 提交自定义任务到指定优先级队列,与 UI 渲染时序对齐。
  3. 自定义 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
    }
}

关键说明

  1. doFrame(currentFrameTimeNanos: Long):currentFrameTimeNanos 是当前帧 VSYNC 信号的时间戳(纳秒),对应帧渲染的起始时刻。
  2. 持续监听:需在 doFrame() 中重新调用 postFrameCallback(this),形成循环;单次监听可省略此行,仅响应一次 VSYNC 信号。
  3. 资源释放:页面不可见时必须调用 removeFrameCallback(),避免内存泄漏和不必要的 CPU 消耗。
  4. 卡顿判定:核心标准是「帧间隔 > 屏幕 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 // 延迟时间(毫秒)
        )
    }
}

关键说明

  1. 任务执行时机:提交的任务会在下一次 VSYNC 信号触发时,按队列优先级执行,避免与核心 UI 任务竞争资源。
  2. 延迟任务:postCallbackDelayed() 支持延迟提交任务,延迟时间到后,需等待下一个 VSYNC 信号才会执行,保证与帧渲染对齐。
  3. 任务移除:可通过 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)
    }
}

关键优势

  1. 避免过度绘制:invalidate() 会触发立即刷新,可能导致同一 VSYNC 周期内多次绘制;而 Choreographer 调度的绘制,每 VSYNC 周期仅执行一次。
  2. 与 VSYNC 对齐:绘制任务与系统 UI 渲染时序一致,减少 CPU/GPU 资源浪费,提升流畅性。

五、 关键注意事项与避坑指南

  1. 线程限制

    • 主线程的 Choreographer 负责 UI 渲染,仅能在主线程执行 UI 相关操作(如 invalidate()、提交 TRAVERSAL 队列任务)。
    • 子线程的 Choreographer 无法操作 UI,仅可用于监听子线程内部的任务调度,无实际 UI 优化价值。
  2. 避免内存泄漏

    • 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 // 引用已回收,直接返回
              // 执行后续逻辑
          }
      }
      
  3. 不依赖帧回调时间戳做精准计时

    • currentFrameTimeNanos 是 VSYNC 信号到达应用进程的时间戳,并非硬件发出的原始时间戳,存在微小延迟,不可用于高精度计时场景。
  4. 避免在高优先级队列提交耗时任务

    • INPUT 和 ANIMATION 队列优先级高,若提交耗时任务(如复杂计算、I/O),会阻塞用户输入和动画执行,导致卡顿。
最近更新:: 2026/1/15 03:00
Contributors: luokaiwen