第4章:帧率监控实战:如何精确测量UI渲染帧率,使用dumpsys SurfaceFlinger
帧率这东西,说白了就是座舱系统的「心跳」。
我做座舱性能优化这些年,最怕听到的就是用户说「卡」。但卡顿到底有多严重?是掉到30帧还是偶尔跳一下?没有数据,你连问题都描述不清楚。
这一章,咱们就聊聊怎么用系统自带的工具——dumpsys SurfaceFlinger,把帧率测准、测透。
4.1 为什么不用第三方工具?
很多人习惯装个GameBench或者PerfDog。嗯,这些工具确实方便,但有个致命问题:它们会引入额外开销。
我在项目里遇到过,用PerfDog测帧率,结果工具本身占用了5%的CPU。你想想看,这5%的负载会不会影响帧率?测出来的数据,到底是谁的帧率?
所以我的习惯是:能用系统工具,就不用第三方。dumpsys SurfaceFlinger是Android原生提供的,零侵入,不占资源。这才是真实数据。
4.2 SurfaceFlinger 是什么?
简单说,SurfaceFlinger是Android的显示合成服务。所有UI图层——状态栏、导航栏、地图、音乐——最终都要交给它来合成,然后输出到屏幕。
它就像一个调度员。每个图层什么时候准备好,什么时候合成,它都一清二楚。所以,问它帧率最准。
核心概念:SurfaceFlinger记录的是「合成帧率」,不是「应用绘制帧率」。两者有区别,但座舱场景下,合成帧率才是用户真正看到的帧率。
4.3 基本命令:dumpsys SurfaceFlinger
打开adb,输入:
adb shell dumpsys SurfaceFlinger
你会看到一大坨输出。别慌,我带你拆解。
首先,找到这段:
DisplayDevice: built-in
current: 16666666
refresh: 16666666
xdpi: 160.0
ydpi: 160.0
这里的refresh单位是纳秒。16666666纳秒换算一下就是16.67ms,对应60fps。如果是11111111,那就是90fps。
嗯,这里要注意:refresh值代表屏幕的硬件刷新率。如果你的座舱屏幕是60Hz,那这个值就是16.67ms。如果显示30fps,那问题就出在内容端,不是屏幕。
4.4 关键指标:掉帧统计
往下翻,找到这段:
Stats since: 2024-01-15 10:23:45
Total frames: 3600
Janky frames: 45
Missed vsync: 12
High latency: 8
Slow UI thread: 15
Slow buffer submission: 10
这些才是核心数据。我来解释一下:
| 指标 | 含义 | 我的经验阈值 |
|---|---|---|
| Total frames | 统计周期内的总帧数 | 无 |
| Janky frames | 掉帧总数(任何原因) | 超过5%就要警惕 |
| Missed vsync | 错过垂直同步信号 | 超过1%说明渲染超时 |
| Slow UI thread | UI线程阻塞 | 超过0.5%需要优化 |
| Slow buffer submission | 缓冲区提交延迟 | 通常和GPU相关 |
我的习惯:每次性能测试前,先跑一次dumpsys SurfaceFlinger --clear清空历史数据。然后跑测试场景,跑完再dump。这样数据才是干净的,不会混入开机阶段的噪声。
4.5 实战:写个脚本持续监控
手动敲命令太累了。我一般写个脚本,每5秒采样一次:
#!/bin/bash
# 帧率监控脚本,每5秒采样一次
while true; do
timestamp=$(date "+%H:%M:%S")
# 提取关键字段
data=$(adb shell dumpsys SurfaceFlinger | grep -A 20 "Stats since")
total=$(echo "$data" | grep "Total frames" | awk '{print $3}')
janky=$(echo "$data" | grep "Janky frames" | awk '{print $3}')
if [ ! -z "$total" ] && [ "$total" -gt 0 ]; then
janky_rate=$(echo "scale=2; $janky * 100 / $total" | bc)
echo "[$timestamp] 总帧: $total, 掉帧: $janky, 掉帧率: $janky_rate%"
fi
sleep 5
done
跑起来之后,你会看到类似这样的输出:
[10:00:05] 总帧: 300, 掉帧: 2, 掉帧率: 0.67%
[10:00:10] 总帧: 600, 掉帧: 5, 掉帧率: 0.83%
[10:00:15] 总帧: 900, 掉帧: 45, 掉帧率: 5.00% ← 这里出问题了!
看到掉帧率突然飙升,你就知道——嗯,某个操作触发了性能瓶颈。
4.6 定位具体哪个图层卡顿
光知道掉帧还不够,你得知道是哪个图层在拖后腿。
继续看dump输出,找到HWC layers部分:
HWC layers:
layer 0: com.android.systemui
visible: true
frame: [0, 0, 1920, 720]
acquireFence: 0x7b4a
releaseFence: 0x7b4b
presentFence: 0x7b4c
...
layer 1: com.example.car.launcher
visible: true
frame: [0, 0, 1920, 720]
acquireFence: 0x7b4d
releaseFence: 0x7b4e
presentFence: 0x7b4f
...
每个图层都有三个关键时间戳:
- acquireFence:图层数据准备完成的时间
- releaseFence:图层数据被消费的时间
- presentFence:实际显示到屏幕的时间
如果某个图层的acquireFence到presentFence的间隔明显大于其他图层,那它就是瓶颈。
我曾经踩过的坑:有一次发现地图图层总是延迟,查了半天发现是地图SDK在后台持续下载离线数据,占满了GPU带宽。所以,帧率问题不一定是UI代码的问题,也可能是后台任务在抢资源。
4.7 进阶:用dumpsys计算瞬时帧率
平均帧率会掩盖瞬间卡顿。我更喜欢看瞬时帧率。
用这个命令:
adb shell dumpsys SurfaceFlinger --latency
输出是一串时间戳,每行代表一帧的完成时间(纳秒):
1705300000000000
1705300016666666
1705300033333332
1705300050000000
1705300066666666
1705300100000000 ← 这里跳了33ms!
1705300116666666
计算相邻两帧的差值:
- 正常:16666666(16.67ms)→ 60fps
- 异常:33333333(33.33ms)→ 30fps
你看,第5帧到第6帧之间,间隔了33ms。这就是一次明显的掉帧。用这个数据,你可以精确到「哪一秒、哪一帧」出了问题。
4.8 总结一下我的工作流
- 清空历史:
dumpsys SurfaceFlinger --clear - 跑测试场景:比如滑动列表、切换页面
- 采集数据:用脚本持续dump,记录时间戳
- 分析掉帧率:看Janky frames占比
- 定位图层:看哪个图层的presentFence延迟最高
- 确认根因:结合Systrace或Simpleperf进一步分析
这套流程我用了好几年,从Android 9到Android 14,基本没变过。工具虽然老,但管用。
下一章,咱们聊聊怎么用Systrace把帧率问题「可视化」出来。到时候你会发现,有些卡顿用dumpsys看不出来,但Systrace里一目了然。