Android帧率监控原理:Choreographer vs SurfaceFlinger

做帧率监控这么多年,我一直觉得有两兄弟绕不开——Choreographer 和 SurfaceFlinger。一个是UI线程的节拍器,一个是合成显示的调度官。你要真想搞清楚帧率为什么掉,或者监控工具有没有漏报误报,就得先弄明白这俩家伙到底在干嘛。

我习惯把帧率监控拆成两个层面:应用侧和系统侧。Choreographer管应用侧,SurfaceFlinger管系统侧。今天咱们就把这俩掰开揉碎了讲。

Choreographer:UI线程的节拍器

Choreographer是Android在4.1引入的玩意,说白了就是给UI线程一个精准的节拍。它监听硬件VSync信号,然后回调你的doFrame方法。你想想看,如果没有这个节拍器,App想啥时候画就啥时候画,那画面撕裂、掉帧都算轻的。

它的工作流程大致是这样:

  1. 硬件VSync到来,Choreographer收到信号
  2. 它通知所有注册的FrameCallback,也就是你的doFrame
  3. App开始执行布局、绘制,然后把生成好的Buffer丢给SurfaceFlinger
关键指标: Choreographer的 doFrame 回调间隔。如果间隔不是16.6ms(60fps),那就是掉帧了。我经常用 Choreographer.getInstance().postFrameCallback() 在代码里手动打log看每次回调的时间差。

我在项目中遇到过一个问题:某个页面滑动时帧率突然从60fps掉到30fps。查半天以为是渲染太重,结果发现是某个第三方SDK在UI线程里做了大量JSON解析,把Choreographer的回调给堵住了。说白了,Choreographer再准,你线程没空处理也没用。

SurfaceFlinger:合成与显示的调度官

SurfaceFlinger工作在系统服务层,它管的是多个窗口(Surface)怎么合成一张图,然后送显。它不直接处理应用绘制,而是等App把Buffer提交过来后,统一做合成。

你想想看,系统里可能有十几个窗口:状态栏、导航栏、你的App、还有通知。每个窗口都有一块Buffer,SurfaceFlinger得决定什么时候把这些Buffer合成,什么时候送到显示屏。这个决策靠的是VSync信号和Fence机制。

避坑指南: 我曾经被SurfaceFlinger的掉帧坑过一次。当时用dumpsys SurfaceFlinger --latency看帧率,发现每过几秒就跳一次大延迟。排查后发现是App频繁创建和销毁Surface,导致SurfaceFlinger得反复做BufferQueue的重建,合成节奏被打乱。后来用固定Surface池解决。

SurfaceFlinger的工作周期一般也是16.6ms,但它和Choreographer并不是完全同步的。Choreographer走的是应用VSync,SurfaceFlinger走的是合成VSync。系统有两个VSync相位,就是为了让App渲染足够的时间,等SurfaceFlinger开始合成时Buffer已经就位。

两者对比

我把核心差异整理成了表格,你对比着看会更清晰:

对比项 Choreographer SurfaceFlinger
工作层级 应用框架层 系统服务层
触发源 应用VSync 合成VSync
主要职责 协调绘制与布局 合成与显示调度
延迟来源 UI线程卡顿 Buffer提交延迟、合成超时
监控方法 Choreographer.FrameCallback dumpsys SurfaceFlinger --latency
常见问题 主线程死锁、过重绘制 多窗口竞争、Fence等待

你看,两者在时间轴上其实是流水线关系:Choreographer催App画,App画完丢Buffer,SurfaceFlinger拿到Buffer开始合,合成完送显。任何一个环节延迟,都会体现在最终帧率上。

实战中如何监控

平时做监控,我习惯双管齐下。在应用侧,我会插入Choreographer回调来记录每一帧的时间戳;在系统侧,我会定期拉取SurfaceFlinger的信息。

先给个写死的代码示例,你可以在自己的监控工具里用:

// 应用侧监控示例
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    long lastFrameTimeNanos = 0;

    @Override
    public void doFrame(long frameTimeNanos) {
        if (lastFrameTimeNanos != 0) {
            long delta = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000L; // 转毫秒
            if (delta > 20) { // 超过20ms算掉帧
                Log.w("FrameMonitor", "掉帧: " + delta + "ms");
            }
        }
        lastFrameTimeNanos = frameTimeNanos;
        Choreographer.getInstance().postFrameCallback(this);
    }
});
技巧: 如果你在系统层做监控,可以直接用 adb shell dumpsys SurfaceFlinger --latency + 你的WindowName。这条命令会输出近期的帧数据,每行是"时间戳 绘制耗时 帧周期"。我通常用它来排查是App渲染慢还是合成慢。

你想想看,光看Choreographer的回调时间,只能知道UI线程有没有卡。但如果帧率低是由SurfaceFlinger合成超时引起的,Choreographer那边是看不出来的。所以一定要两边数据对齐,才能准确定位。

嗯,最后提醒一句:监控工具本身也会消耗性能,千万别在线上App里高频轮询。我的经验是采样率控制在每秒一次,然后把数据先缓存在内存里,等帧率真掉的时候再详细记录。

Choreographer vs SurfaceFlinger 帧率监控架构 Vsync源(硬件) Choreographer App渲染线程 BufferQueue SurfaceFlinger 显示屏 通知 监控要点: 应用侧通过Choreographer回调查看UI线程耗时;系统侧通过SurfaceFlinger的Buffer合成延迟定位瓶颈。 两者结合才能区分“App绘制慢”还是“合成送显慢”。