第3章:Choreographer:编舞者工作机制与帧率控制

说到Android的渲染性能,绕不开一个核心角色——Choreographer。这名字挺有意思,直译过来是“编舞者”。

它到底在编排什么?说白了,就是在编排每一帧的绘制节奏。

我刚开始接触Android图形栈时,总觉得帧率卡顿就是代码写得慢。后来才发现,很多时候不是代码慢,而是节奏乱了。Choreographer就是那个帮你稳住节奏的“指挥家”。

3.1 Choreographer的诞生背景

在Android 4.1之前,系统没有统一的帧率控制机制。应用想什么时候画就什么时候画,想怎么画就怎么画。

结果呢?

你想想看,如果UI线程在绘制时,突然来了个触摸事件,或者网络回调,整个绘制流程就被打断了。画面就会出现掉帧、撕裂,甚至卡死。

我记得在早期项目里,有个页面滑动起来总是一顿一顿的。排查了半天,发现是某个第三方SDK在后台频繁刷新UI。那时候没有Choreographer,系统根本管不住这种“乱画”的行为。

核心问题:Android 4.1之前,应用层和系统层的绘制是“异步且无序”的。应用层不知道Vsync什么时候来,系统层也不知道应用层什么时候画完。

Google的工程师们意识到,必须引入一个“调度者”。这个角色就是Choreographer。

它的职责很简单:

  • 监听Vsync信号
  • 按优先级分发绘制任务
  • 统计每一帧的执行时间
  • 反馈掉帧信息

3.2 Vsync信号:心跳的节奏

Choreographer的工作基础,是Vsync信号。你可以把它理解成屏幕的“心跳”。

屏幕刷新是有固定频率的,比如60Hz、90Hz、120Hz。每次刷新前,硬件会发出一个Vsync信号,告诉系统:“我要开始刷新了,你准备好数据了吗?”

Choreographer就是在这个信号到来时,开始分发任务。

个人经验:我曾经在调试一个90Hz屏幕的平板时,发现帧率始终跑不满。后来用Systrace抓了一下,发现Choreographer收到的Vsync信号间隔不均匀。排查到最后,是某个驱动层的电源管理策略在作怪。嗯,这种问题应用层很难感知,但帧率就是上不去。

Vsync信号的流程大致如下:

  1. 硬件产生Vsync信号
  2. SurfaceFlinger接收并处理
  3. Choreographer通过Binder接收回调
  4. 开始分发绘制任务

这里有个关键点:Choreographer接收的是“软件Vsync”,不是硬件Vsync。软件Vsync是SurfaceFlinger模拟出来的,目的是让应用层和系统层的节奏对齐。

3.3 Choreographer的工作流程

咱们来拆解一下Choreographer的具体工作流程。我习惯把它分成三个阶段:

3.3.1 注册回调

应用层通过Choreographer.getInstance().postFrameCallback()注册一个回调。这个回调会在下一个Vsync到来时被执行。

// 注册一个帧回调
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
        // 在这里执行你的绘制逻辑
        // frameTimeNanos 是当前帧的时间戳
    }
});

你可能会问:为什么不用Handler?

因为Handler是基于消息队列的,它不关心Vsync。你用Handler post一个Runnable,它会在下一个Looper循环中执行,但那个时间点可能刚好在Vsync中间,导致绘制结果要等到下一个Vsync才能显示。

说白了,Handler只管“执行”,不管“时机”。Choreographer管的是“时机”。

3.3.2 接收Vsync

当Vsync信号到来时,Choreographer会从回调队列中取出所有待执行的回调,按优先级排序:

  • 输入事件处理(最高优先级)
  • 动画处理(比如属性动画、View动画)
  • 遍历/绘制(View的measure、layout、draw)

这个优先级顺序是有讲究的。你想想看,如果先处理绘制,再处理输入事件,那用户触摸屏幕后,画面要等到下一帧才能响应。体验就很差了。

3.3.3 执行回调

Choreographer会依次执行这些回调。每个回调执行完后,会检查是否超时。

注意:Choreographer本身并不执行绘制代码。它只是“通知”应用层该画了。真正的绘制逻辑还是在UI线程的Handler中执行的。所以,如果你的UI线程被其他任务阻塞了,Choreographer通知了也没用。

3.4 帧率控制与掉帧检测

Choreographer还有一个重要职责:检测掉帧。

每次doFrame被调用时,Choreographer会计算当前帧的时间戳和上一帧的时间戳之差。如果这个差值超过了Vsync周期的两倍,就说明发生了掉帧。

// Choreographer内部的大致逻辑(简化版)
void doFrame(long frameTimeNanos, int frame) {
    long startNanos = System.nanoTime();
    long jitterNanos = startNanos - frameTimeNanos;
    
    if (jitterNanos >= 2 * vsyncPeriodNanos) {
        // 掉帧了!
        // 记录掉帧数
        skippedFrames = jitterNanos / vsyncPeriodNanos;
        Log.w(TAG, "Skipped " + skippedFrames + " frames!");
    }
    
    // 执行回调...
}

这个掉帧日志,你肯定在Logcat里见过:

Skipped 47 frames!  The application may be doing too much work on its main thread.

我曾经在一个直播项目中,看到这个日志刷屏。当时主播的礼物特效一多,画面就卡成PPT。排查后发现,是某个自定义View在onDraw里做了大量的Bitmap解码操作。

嗯,这种问题其实很好定位。Choreographer已经把答案写在日志里了——主线程做了太多工作。

3.5 帧率统计工具

Choreographer本身不提供直接的帧率API,但我们可以通过它的回调机制来统计帧率。

public class FpsMonitor {
    private long lastFrameTime = 0;
    private int frameCount = 0;
    private float fps = 0;
    
    public void start() {
        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long frameTimeNanos) {
                long now = System.currentTimeMillis();
                if (lastFrameTime == 0) {
                    lastFrameTime = now;
                }
                
                frameCount++;
                long elapsed = now - lastFrameTime;
                
                if (elapsed >= 1000) {
                    fps = frameCount * 1000f / elapsed;
                    frameCount = 0;
                    lastFrameTime = now;
                    Log.d("FpsMonitor", "Current FPS: " + fps);
                }
                
                // 继续注册下一帧
                Choreographer.getInstance().postFrameCallback(this);
            }
        });
    }
}

这个工具虽然简单,但在开发阶段非常实用。我习惯把它放在Debug面板里,随时观察帧率变化。

避坑指南:千万不要在线上版本中频繁调用postFrameCallback。因为每次注册都会增加Choreographer的回调队列长度,反而会影响性能。我一般只在Debug包中使用,或者通过反射控制开关。

3.6 Choreographer的局限性

Choreographer虽然强大,但也不是万能的。我总结了几点局限性:

局限性 说明
只能控制主线程 Choreographer的回调在主线程执行,无法控制子线程的绘制
无法解决渲染管线瓶颈 如果GPU渲染速度跟不上,Choreographer也无能为力
回调队列可能堆积 如果主线程卡顿,回调会堆积,导致更严重的掉帧
无法感知SurfaceFlinger的合成延迟 Choreographer只管“通知”,不管“合成”

说白了,Choreographer是一个“调度者”,不是“执行者”。它能把节奏稳住,但如果你本身的代码质量不行,它也没办法帮你把帧率提上去。

3.7 实战建议

最后,分享几个我在项目中积累的实战建议:

  1. 善用掉帧日志:Logcat里出现“Skipped frames”时,不要忽略。这是Choreographer在告诉你,你的主线程超负荷了。
  2. 避免在doFrame中做耗时操作:doFrame回调里只做轻量级操作,比如更新UI状态。复杂的计算放到子线程。
  3. 合理使用postFrameCallback:不要频繁注册和取消,尽量保持稳定的回调链。
  4. 结合Systrace使用:Choreographer的调度信息在Systrace中一目了然。我习惯用Systrace抓取掉帧时的trace,定位具体是哪个阶段耗时。
  5. 注意Vsync偏移:不同设备的Vsync偏移量不同。在调试帧率时,最好在真机上测试,模拟器数据不准。

嗯,关于Choreographer的内容,今天就先聊到这里。下一章我们会深入SurfaceFlinger,看看它到底是怎么把各个应用的图层合成到一起的。