4. 计算实时帧率与单帧耗时:帧间隔计算、FPS 公式、毫秒级精度处理

帧率监控的核心是从相邻两帧的时间差中提取出两个关键指标:实时 FPS 和 单帧耗时。Choreographer 提供的回调时间戳精度为纳秒级,直接转换即可得到毫秒级帧间隔。本章从数学原理出发,逐步拆解时间戳采集、帧间隔计算、FPS 公式推导以及高精度转换的实现细节。

4.1 帧间隔与 FPS 的数学关系

帧间隔(Frame Interval)定义为连续两帧之间的时间差,单位通常为毫秒(ms)。FPS(Frames Per Second)与帧间隔互为倒数关系,计算公式为:

FPS = 1000 / Δt(ms)

下表列出常见帧间隔与对应 FPS,便于开发时快速对照评估性能等级:

帧间隔 Δt (ms)FPS流畅度等级典型场景
33.3330低视频播放 / 入门设备
22.2245中等中端手机日常使用
16.6760流畅主流旗舰标准
11.1190高刷90Hz 屏幕
8.33120电竞120Hz 游戏
6.94144极速144Hz 专业电竞
4.17240超高分240Hz 触控采样屏
关键结论:帧间隔越短,FPS 越高;当帧间隔超过 16.67ms 时,用户肉眼即可感知卡顿。掉帧的本质就是帧间隔突然跳升到 33ms 甚至 50ms 以上。
帧间隔计算与 FPS 输出流程 帧间隔采集 → FPS 计算 → 单帧耗时输出 Choreographer 帧回调 获取时间戳 tₙ (纳秒) 计算帧间隔 Δt = tₙ − tₙ₋₁ 帧间隔值 Δt (ms) FPS 计算 FPS = 1000/Δt 单帧耗时 = Δt ms 实时监控 帧率 | 耗时 帧间隔是连续两帧之间的时间差,直接决定 FPS 高低;单帧耗时在数值上等于帧间隔,用于定位具体哪一帧过重。

4.2 基于 Choreographer 的帧间隔采集实现

通过 Choreographer.FrameCallback 的 doFrame(long frameTimeNanos) 可获取纳秒级时间戳。每次回调记录当前时间戳并与上一次相减即得帧间隔。首次回调需要丢弃,因为没有前序参考点。

下面是一个最简的帧间隔采集与 FPS 计算实现:

public class SimpleFpsTracker implements Choreographer.FrameCallback {
    private static final String TAG = "FpsTracker";
    private long lastFrameTimeNs = 0;
    private long frameCount = 0;

    @Override
    public void doFrame(long frameTimeNanos) {
        if (lastFrameTimeNs == 0) {
            // 首次回调,仅记录时间戳,不计算
            lastFrameTimeNs = frameTimeNanos;
            Choreographer.getInstance().postFrameCallback(this);
            return;
        }

        // 计算帧间隔(纳秒)
        long intervalNs = frameTimeNanos - lastFrameTimeNs;
        // 转换为毫秒(保留 1 位小数)
        double intervalMs = intervalNs / 1_000_000.0;
        // 计算瞬时 FPS
        double fps = 1000.0 / intervalMs;

        Log.d(TAG, String.format("帧间隔: %.2f ms | 瞬时 FPS: %.1f", intervalMs, fps));

        lastFrameTimeNs = frameTimeNanos;
        frameCount++;
        Choreographer.getInstance().postFrameCallback(this);
    }

    public void start() {
        Choreographer.getInstance().postFrameCallback(this);
    }

    public void stop() {
        Choreographer.getInstance().removeFrameCallback(this);
    }
}
注意: frameTimeNanos 是 Choreographer 内部统一的时间基准(CLOCK_MONOTONIC),不受系统时间跳变影响,适合做高精度间隔测量。

4.3 毫秒级精度转换与滑动窗口 FPS

直接使用单帧间隔计算出的瞬时 FPS 波动大,不适合作为稳定性评判依据。实际工程中采用滑动窗口平均帧间隔来平滑噪声,窗口大小通常取 30~60 帧(对应 0.5s~1s)。

以下实现维护一个环形缓冲区,存储最近 N 帧的间隔(纳秒),过滤异常值后计算平均 FPS:

public class SmoothFpsMonitor {
    private static final int WINDOW_SIZE = 60;
    private final long[] intervalsNs = new long[WINDOW_SIZE];
    private int index = 0;
    private int count = 0;
    private long lastFrameTimeNs = 0;

    // 每帧回调时调用此方法
    public void onFrame(long frameTimeNanos) {
        if (lastFrameTimeNs == 0) {
            lastFrameTimeNs = frameTimeNanos;
            return;
        }

        long intervalNs = frameTimeNanos - lastFrameTimeNs;
        lastFrameTimeNs = frameTimeNanos;

        // 过滤异常:小于 1ms 或大于 200ms 的间隔直接丢弃
        if (intervalNs < 1_000_000 || intervalNs > 200_000_000) {
            return;
        }

        // 写入环形缓冲区
        intervalsNs[index] = intervalNs;
        index = (index + 1) % WINDOW_SIZE;
        if (count < WINDOW_SIZE) count++;
    }

    // 获取当前平均帧间隔 (ms) 和 FPS
    public double[] getAverage() {
        if (count == 0) return new double[]{0, 0};

        long sum = 0;
        int valid = 0;
        for (int i = 0; i < count; i++) {
            if (intervalsNs[i] > 0) {
                sum += intervalsNs[i];
                valid++;
            }
        }
        if (valid == 0) return new double[]{0, 0};

        double avgIntervalMs = (sum / (double) valid) / 1_000_000.0;
        double avgFps = 1000.0 / avgIntervalMs;
        return new double[]{avgIntervalMs, avgFps};
    }
}

毫秒级精度处理要点:

  • 使用 double 而非 float 做除法,保留小数点后 2 位有效数字。
  • 纳秒转毫秒时除以 1_000_000.0 而非 1_000_000,避免整数截断。
  • 输出用 String.format("%.2f", value) 控制精度,便于日志对齐。
// 精度处理示例
long intervalNs = 16_666_666;          // 约 16.67ms
double intervalMs = intervalNs / 1_000_000.0;   // → 16.666666
double fps = 1000.0 / intervalMs;               // → 60.000000
String report = String.format("间隔: %.2f ms, FPS: %.1f", intervalMs, fps);
// 输出: "间隔: 16.67 ms, FPS: 60.0"

4.4 常见精度陷阱与避坑指南

陷阱 1:首次回调不丢弃
第一次 doFrame 的 frameTimeNanos 与 0 相减会得到一个巨大的帧间隔,导致 FPS 趋近于 0。必须显式判断 lastFrameTimeNs == 0 跳过第一次计算。
陷阱 2:整数除法截断
long intervalMs = intervalNs / 1000000; 会丢失小数部分,使帧间隔偏小、FPS 虚高。始终使用 intervalNs / 1_000_000.0 保持 double 精度。
陷阱 3:异常值污染窗口
应用冷启动、Activity 切换、GC 停顿等场景会产生极端帧间隔(如 500ms)。滑动窗口必须加入阈值过滤,一般保留 1ms~200ms 范围内的间隔。
最佳实践:将帧间隔原始数据写入本地日志或 trace 文件,结合 Systrace 或 Perfetto 进行归因分析。仅凭 FPS 数字无法定位具体掉帧原因,必须配合单帧耗时的时间戳上下文。

完成本章后,你已经掌握了从 Choreographer 原始纳秒时间戳到实时 FPS 和单帧耗时的完整计算链路。下一环节将基于此链路构建掉帧检测器,并定义“掉帧阈值”与“卡顿分级标准”。