4、卡顿的测量:如何量化卡顿、FPS监控的实现、帧耗时分析、使用adb或工具抓取帧数据

卡顿这东西,你说它存在吧,肉眼确实能感觉到。你说它不存在吧,代码跑起来又没报错。所以,咱们得用数据说话。

我刚开始做性能优化那会儿,也犯过“凭感觉”的毛病。觉得界面挺流畅的,结果一上真机,用户反馈卡成PPT。后来我学乖了——没有量化,就没有优化。这一章,咱们就聊聊怎么把卡顿“抓”出来,让它无处遁形。

4.1 卡顿的量化指标:别光靠眼睛

你想想看,一个界面卡不卡,最直观的感受是什么?是“掉帧”。

正常情况下,屏幕每秒刷新60次,也就是每帧16.6ms。如果某一帧绘制时间超过了这个阈值,下一帧就得等,肉眼就会察觉到“跳了一下”。

我个人习惯用三个核心指标来量化卡顿:

  • FPS(Frames Per Second):每秒渲染的帧数。60fps是黄金标准,低于30fps基本就属于“灾难现场”。
  • 帧耗时(Frame Time):每一帧从开始到结束的耗时。超过16.6ms的帧,就是“掉帧”。
  • 卡顿率(Jank Rate):掉帧帧数占总帧数的比例。比如100帧里掉了5帧,卡顿率就是5%。

重点来了:FPS高不代表不卡顿。比如你前10帧跑得飞快,第11帧突然卡了500ms,平均FPS可能还是50多,但用户已经骂娘了。所以,帧耗时的分布比平均值更重要。

4.2 FPS监控的实现:自己动手写一个

很多现成的工具都能看FPS,比如Android自带的Profile GPU Rendering。但作为工程师,咱们得知道底层原理。说白了,FPS监控就是“数帧”。

在Android里,我们可以通过 Choreographer 来监听每一帧的回调。我给大家写个简单的示例:

public class FpsMonitor {
    private long lastFrameTime = 0;
    private int frameCount = 0;
    private long startTime = 0;

    public void start() {
        startTime = System.nanoTime();
        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long frameTimeNanos) {
                long now = System.nanoTime();
                if (lastFrameTime != 0) {
                    long delta = now - lastFrameTime;
                    // 帧耗时超过16.6ms,记录为掉帧
                    if (delta > 16_600_000) {
                        frameCount++;
                    }
                }
                lastFrameTime = now;

                // 每秒计算一次FPS
                if (now - startTime > 1_000_000_000) {
                    float fps = frameCount * 1.0f / ((now - startTime) / 1_000_000_000f);
                    Log.d("FPS", "当前FPS: " + fps);
                    frameCount = 0;
                    startTime = now;
                }

                // 继续监听下一帧
                Choreographer.getInstance().postFrameCallback(this);
            }
        });
    }
}

这段代码的逻辑很简单:每次屏幕刷新时,Choreographer 会回调 doFrame 方法。我们记录两次回调的时间差,如果超过16.6ms,就计一次掉帧。每秒统计一次,就能算出实时的FPS。

小提示:Choreographer 的回调是在UI线程执行的。如果你在回调里做耗时操作,反而会加剧卡顿。所以,只做记录,别做计算。我一般会把数据扔到后台线程去处理。

4.3 帧耗时分析:找到“罪魁祸首”

光知道FPS低还不够,你得知道是哪一帧卡了,以及为什么卡。这就需要分析帧耗时了。

Android Studio自带的 CPU Profiler 和 GPU Profiler 都能看到每一帧的耗时分布。但如果你像我一样,喜欢在命令行里干活,那 adb shell dumpsys gfxinfo 就是你的好帮手。

举个例子,抓取某个App的帧数据:

adb shell dumpsys gfxinfo com.example.myapp

输出结果里,你会看到类似这样的表格:

帧序号 Draw耗时(ms) Prepare耗时(ms) Process耗时(ms) Execute耗时(ms) 总耗时(ms)
1 2.1 1.5 8.3 4.7 16.6
2 3.0 2.0 12.5 5.0 22.5
3 1.8 1.2 6.0 3.5 12.5

看到第2帧了吗?总耗时22.5ms,明显超了。再往下看,Process阶段花了12.5ms,这通常意味着布局测量或者绘制逻辑出了问题。

注意:dumpsys gfxinfo 抓取的是最近128帧的数据。如果你的App跑得很快,可能抓不到卡顿的那一帧。我建议你配合 adb logcat 一起用,或者用 dumpsys gfxinfo --framestats 获取更详细的帧时间戳。

4.4 使用adb或工具抓取帧数据

除了手动敲命令,还有一些工具能帮你自动化抓取和分析。我常用的有这几个:

  • Perfetto:Google官方的性能分析工具,能抓取CPU、GPU、内存、帧数据等。用 adb shell perfetto 命令启动,生成的文件可以在浏览器里打开看。
  • Systrace:老牌工具了,虽然有点过时,但胜在稳定。用 python systrace.py 抓取,能看到每一帧的渲染流水线。
  • py-framerate:一个Python小脚本,专门用来解析 dumpsys gfxinfo 的输出,生成可视化图表。

我个人比较推荐 Perfetto。它不仅能看帧数据,还能把CPU调度、线程状态、GPU渲染全部关联起来。有一次我排查一个诡异的卡顿问题,就是靠Perfetto发现是某个后台线程抢占了CPU时间片,导致UI线程被饿死。

下面这张图,是我用Perfetto抓到的帧数据可视化结果。你可以看到每一帧的耗时分布,以及哪些帧超过了16.6ms的阈值:

帧耗时分布图(Perfetto可视化) 0ms 16.6ms 33.3ms 50ms 16.6ms阈值 帧1 帧2 帧3 帧4 帧5 帧6 帧7 帧8 帧9 帧10 正常帧 掉帧

你看,帧6和帧9明显超过了16.6ms的阈值,这就是我们需要重点排查的对象。点击对应的帧,Perfetto会展示出那一帧里所有线程的活动,包括UI线程在做什么、GPU在渲染什么、有没有锁竞争等等。

避坑指南:我曾经用 dumpsys gfxinfo 抓数据时,发现帧耗时忽高忽低,以为是代码问题。后来才发现,是因为手机连着USB充电,CPU被强制降频了。所以,抓数据时尽量用电池供电,或者关掉充电优化。

4.5 小结:量化是优化的第一步

这一章咱们聊了怎么量化卡顿。从FPS到帧耗时,从 Choreographer 到 dumpsys gfxinfo,再到Perfetto这样的专业工具。说白了,就是要把“感觉卡”变成“数据卡”。

有了数据,你才能知道问题出在哪。是布局太复杂?是动画太频繁?还是主线程被阻塞了?下一章,咱们就深入分析这些卡顿的根源。

嗯,先到这。记住,没有数据,别谈优化。