第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:实际显示到屏幕的时间

如果某个图层的acquireFencepresentFence的间隔明显大于其他图层,那它就是瓶颈。

我曾经踩过的坑:有一次发现地图图层总是延迟,查了半天发现是地图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 总结一下我的工作流

  1. 清空历史dumpsys SurfaceFlinger --clear
  2. 跑测试场景:比如滑动列表、切换页面
  3. 采集数据:用脚本持续dump,记录时间戳
  4. 分析掉帧率:看Janky frames占比
  5. 定位图层:看哪个图层的presentFence延迟最高
  6. 确认根因:结合Systrace或Simpleperf进一步分析

这套流程我用了好几年,从Android 9到Android 14,基本没变过。工具虽然老,但管用。

下一章,咱们聊聊怎么用Systrace把帧率问题「可视化」出来。到时候你会发现,有些卡顿用dumpsys看不出来,但Systrace里一目了然。