Android 的平滑体验通常被直观描述为滑动不卡顿、动画不掉帧,但在测试工程中,这个目标必须转化为可以采集、统计和回归的量化指标。Smoothing 平滑测试并不是简单地在设备上跑一遍应用看是否崩溃,而是要回答三个问题:每一帧是否按时渲染、掉帧集中在什么场景、主线程和渲染管线的耗时是否超出预算。要准确回答这些问题,测试方案需要同时覆盖帧率统计、帧耗时分布与系统级渲染追踪。

从渲染链路看,Android 屏幕通常以 60Hz 或 120Hz 刷新,60Hz 设备每帧窗口约为 16.67 毫秒。系统通过 Choreographer 协调输入、动画、测量、布局、绘制和合成。如果主线程某一帧的执行时间超过一个刷新周期,就会产生掉帧。用户感受到的卡顿不一定来自平均帧率下降,而常常来自极少数 100 毫秒以上的长帧。因此平滑测试的核心指标不能只看平均 FPS,还要关注帧耗时的 P95、P99 以及卡顿帧占比。
一、核心指标:从平均帧率转向帧耗时分布
平均帧率是最容易理解但也最容易掩盖问题的指标。假设一个页面在 1 秒内渲染 60 帧,平均帧率是 60FPS,但其中可能有 55 帧在 10 毫秒内完成,剩下 5 帧每帧耗时超过 80 毫秒。用户会在快速滑动时明显感到停顿,而平均帧率仍然表现正常。因此平滑测试需要引入帧耗时和长尾指标。
常见的关键指标包括:平均帧耗时、P95 帧耗时、P99 帧耗时、掉帧数、卡顿帧率。卡顿帧一般定义为帧耗时超过 2 个垂直同步周期,例如 60Hz 设备上超过 33.33 毫秒的帧。更严格的应用会使用 1.5 个周期作为阈值,因为视觉系统对细微停顿也很敏感。帧耗时需要区分主线程执行时间和 GPU 合成时间,否则容易把等待 SwapBuffers 的时间误判为主线程耗时。
采集帧耗时最简单的方式是在 Choreographer 中注册 FrameCallback,在回调里计算相邻两帧的时间差。如下面的代码会持续输出每一帧的耗时,并标记超过 20 毫秒的帧。
import android.util.Log;
import android.view.Choreographer;
import java.util.concurrent.TimeUnit;
public class FrameMonitor implements Choreographer.FrameCallback {
private long lastFrameTimeNanos = 0L;
public void start() {
Choreographer.getInstance().postFrameCallback(this);
}
@Override
public void doFrame(long frameTimeNanos) {
if (lastFrameTimeNanos != 0L) {
long frameCost = frameTimeNanos - lastFrameTimeNanos;
long frameCostMs = TimeUnit.NANOSECONDS.toMillis(frameCost);
if (frameCostMs > 20) {
Log.w("FrameMonitor", "jank frame cost=" + frameCostMs + "ms");
}
}
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
}
这种方式的优点是不依赖系统权限,可以集成到应用内测试包中,在指定页面自动采集。缺点是只能拿到帧间隔,无法直接拆解测量、布局、绘制等阶段的耗时。因此这种方式适合作为冒烟测试和场景遍历时的基础监控,深度定位仍需要配合系统级工具。
二、系统工具采集:dumpsys gfxinfo 与 Perfetto 的配合
Android 提供 dumpsys gfxinfo 命令,可以输出应用近期帧渲染的详细阶段数据。执行 adb shell dumpsys gfxinfo com.demo.smooth framestats 后,系统会返回从 IntendedVsync 到 FrameCompleted 的多列数据,每一
Android_Smoothing帧率监控JankStats修改时间:2026-08-13 06:23:09