1、认识帧率监控:什么是帧率、为什么需要监控、Compose UI中的帧率概念
1.1 帧率到底是什么?
说白了,帧率就是手机屏幕每秒钟能刷新多少张画面。单位是 fps(frames per second,帧每秒)。
你想想看,电影是 24fps,我们觉得流畅;游戏要到 60fps 才叫顺滑。在手机 UI 上,Android 系统标准是 60fps,也就是每帧只有 16.67ms 的绘制时间。
我记得刚入行那会儿,以为帧率只是游戏开发才关心的事。直到有一次做列表滑动优化,发现用户反馈“卡得不想用”,一查帧率掉到了 20fps… 嗯,从那以后我再也不敢小看这个指标了。
1.2 为什么我们要监控帧率?
监控帧率就一个目的——发现问题。卡顿、掉帧、画面撕裂、应用发热,这些用户能感知的负面体验,背后几乎都是帧率出了问题。
我在项目中曾经遇到过一个诡异的问题:同样的机型,有的用户说滑动流畅,有的说掉帧严重。最后抓帧率日志发现,掉帧用户的后台进程一直在占用 CPU。如果没有帧率监控,这种偶发问题根本没法定位。
几个必监控的原因:
- 衡量 UI 流畅度的量化指标,比“感觉卡”更客观
- 发现性能瓶颈(比如布局过深、过度绘制、AsyncTask 阻塞主线程)
- 在 CI/CD 中自动拦截引入掉帧的代码提交
- 对比不同版本或不同设备的性能表现
1.3 Compose UI 中的帧率概念
在 Compose 之前,大家用 View 系统,帧率问题主要靠 Choreographer 的回调来查。但 Compose 引入了自己的渲染管线,分为三个阶段:Composition(重组)、Layout(布局)、Drawing(绘制)。
三个阶段都在主线程完成,而且 Compose 是声明式驱动,状态变化会触发范围不同的重组。这意味着——一个看似微小的 State 更新,可能引发整棵 UI 树重绘,帧率瞬间崩掉。
举个例子:我做过一个聊天列表,每条消息都用 remember { mutableStateOf(…) } 保存是否已读。结果用户在快速滑动时,后台推送大量已读状态更新,每次更新都触发整个列表重组。帧率从 55fps 直接掉到 15fps。
这些问题,只有通过帧率监控才能第一时间捕捉到。Compose 官方其实提供了 ComposeFrameMetrics 接口,但埋点比较底层,所以我们需要封装成好用的插件。
1.4 帧率监控的核心指标
光看一个平均 fps 不够。我个人习惯至少看这四个维度:
| 指标 | 说明 | 参考阈值 |
|---|---|---|
| 平均帧率 (Avg FPS) | 一段时间内的帧率均值 | ≥ 55 fps 为优秀,< 30 fps 为严重卡顿 |
| 帧时间 (Frame Time) | 每帧消耗的毫秒数 | ≤ 16.67ms (60fps),> 30ms 用户开始感知卡顿 |
| 掉帧率 (Jank Rate) | 帧时间超过 16.67ms 的占比 | ≤ 5% 可接受,> 10% 需优化 |
| 最长帧 (Worst Frame) | 单帧最大耗时 | 超过 100ms 属于严重异常 |
1.5 帧率监控工具概览
Android 生态里其实有不少现成工具:
- Profile GPU Rendering(开发者选项里的柱状图)—— 只能看不能导出数据
- Systrace / Perfetto —— 强大但要手动抓 trace,不适合自动化
- FrameMetricsAggregator —— 官方 API,但 Compose 场景下需要额外适配
- Choreographer.FrameCallback —— 底层回调,自己计算帧时间
我们的课程要做的 Compose UI 帧率监控插件,核心就是封装 FrameCallback 和 Compose 的 snapshot 特性,在 UI 测试中自动采集、上报、断言帧率数据。说白了,就是让测试用例能这样写:
composeTestRule.measureFrames {
// 执行 UI 操作
onNodeWithTag("list").performScrollToIndex(50)
// 断言
assertThat(avgFps).isAtLeast(55f)
assertThat(jankRate).isLessThan(0.05f)
}
接下来的章节我们会一步步拆解这个插件的实现。下面这张图帮你理清本章的知识结构: