第2章:列表滑动性能测量基线 —— BaselineProfile、FrameTiming 与 dumpsys gfxinfo
优化任何列表性能之前,必须建立可复现、可量化的测量手段。本章从原理与实操两个层面讲解三种核心测量工具:BaselineProfile(编译预热)、FrameTiming(帧耗时)与 dumpsys gfxinfo(GPU管线)。三者互为支撑,覆盖从编译、UI线程到渲染管线的完整性能追踪。
课程建议:在开始本机优化前,先在一台干净的测试设备上(无开发者选项乱改值)运行一次本章所有命令,保存原始结果作为基线。
2.1 三种工具定位与原理
| 工具 | 测量阶段 | 核心原理 | 典型输出指标 |
|---|---|---|---|
| BaselineProfile | 应用启动→首帧 | AOT预编译热路径字节码,减少解释执行与JIT压力 | Cold start frame time / FPS |
| FrameTiming | 滚动过程中UI帧 | 通过 Choreographer 在每帧渲染前、后打点,统计帧耗时 | Frame duration / vsync jitter / dropped frames |
| dumpsys gfxinfo | GPU渲染流水线 | 读取GPU帧缓冲队列的详细渲染阶段耗时(Draw/Prepare/Process/Execute) | Total frame time vs. 16.67ms budget / overdraw |
2.2 BaselineProfile 实现流程
生成Profiling-Rules 文件
在 app/src/main 下创建 baseline-prof/gaming-profile-baseline-prof.txt,内容以 FQN+方法方式描述首个滑动页面热路径。例如:
Landroidx/recyclerview/widget/RecyclerView;->onLayout(Z)V
Lcom/example/app/data/list/ListItemViewHolder;->bind(Ljava/lang/Object;)V
Lcom/example/app/ui/home/HomeScreenKt;->HomeScreen(Landroidx/compose/runtime/Composer;I)V
集成到构建
在模块 build.gradle 添加:
android {
// ...
buildTypes {
release {
// ...
baselineProfile.enabled = true
}
}
}
构建后 APK 内会包含 assets/dexopt/baseline.prof,安装后系统自动进行 AOT 编译。
验证是否生效
adb shell pm dump com.example.app | grep -A 10 "baseline"
# 输出应包含 "baseline profile: true" 和对应方法数
常见陷阱:BaselineProfile 必须在production release构建中开启(debug 构建默认关闭)。另外,Profile 文件中的方法签名不可包含行号,否则编译失败。
2.3 FrameTiming 采集
Choreographer 回调埋点(推荐)
在 Compose 中监听帧耗时:
// 在Modifier或SideEffect中注册
val frameMetricsHandler = remember { FrameMetricsManager() }
LaunchedEffect(Unit) {
withFrameMillis { startMs ->
// 下一帧可用时回调
frameMetricsHandler.recordFrame(timestampMs = startMs)
}
}
更简单的方法:使用 ThreadedRenderer 提供的 API(需 API 29+):
val window = (context as Activity).window
window.addOnFrameMetricsAvailableListener(
{ _, frameMetrics, _ ->
val totalDuration = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION)
val budget = 16_666_666 // 16.67ms in ns
if (totalDuration > budget) {
Log.w("FrameTiming", "Dropped frame: ${totalDuration / 1_000_000}ms")
}
},
Handler(Looper.getMainLooper())
)
手动触发“帧绘制”基准操作
在 Profiler 中录制 5~10 秒的列表快速滑动,然后导出帧分布。但课程推荐使用程序化抓取:
// 在滑动测试开始/结束处打标
adb shell dumpsys gfxinfo com.example.app framestats
# 会输出最近 128 帧的详细耗时
2.4 dumpsys gfxinfo 命令详解
最直接的 GPU 开销查询方式。命令如下:
adb shell dumpsys gfxinfo com.example.app framestats --reset
输出示例(节选):
Flags,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,...
DrawDuration,PrepareDuration,ProcessDuration,ExecuteDuration
0,N,N,N,N,...
333680000,34567733,111111,...
...
解读字段:
- DrawDuration:CPU 侧 draw 指令耗时。
- PrepareDuration:GPU 驱动程序准备命令缓冲。
- ProcessDuration:GPU 工作完成时间(包括等待)。
- ExecuteDuration:实际 GPU 执行时间。
超过 16.67ms 的帧就是“丢帧帧”,可用脚本统计:
adb shell dumpsys gfxinfo com.example.app framestats | tail -n +5 | awk -F',' '{if ($9 > 16666666) print}' | wc -l
2.5 调试与基线建立
每个测量工具的输出都需要结构化保存。建议建立如下目录:
project_root/performance_baselines/
├── cold_start/
│ ├── baseline_profile.json
├── scroll_60fps/
│ ├── frame_timings_20250210.csv
│ ├── gfxinfo_dump.txt
└── scroll_120fps/
└── ...
使用 Python 或 Julia 写一个极简脚本批量解析 dumpsys 输出并生成摘要表:
import subprocess, json, csv
def get_gfx_summary(package):
result = subprocess.run(
['adb', 'shell', 'dumpsys', 'gfxinfo', package, 'framestats'],
capture_output=True, text=True
)
lines = result.stdout.strip().split('\n')[5:] # 跳过标题
timings = []
for line in lines:
parts = line.split(',')
if len(parts) >= 9:
total_ns = int(parts[8]) # 第9列是总耗时
timings.append(total_ns)
return {'total_frame_count': len(timings),
'over_budget_count': sum(1 for t in timings if t > 16_666_666),
'max_ms': max(timings)/1_000_000}
print(get_gfx_summary('com.example.app'))
2.6 常见错误 & 优化技巧
错误:BaselineProfile 文件根路径放置错误
应将配置文件置于
应将配置文件置于
src/main/baseline-prof/,而非 src/debug/ 或 src/release/,否则 release 构建不会包含。检查 build/outputs/mapping/release/ 下是否有 baseline.prof.txt。
技巧:FrameTiming 与 dumpsys 结合分析
当
当
FrameTiming 监控到丢帧,立即运行 dumpsys gfxinfo 获取详细流水线耗时,定位瓶颈在 Draw 还是 Prepare/Process 阶段。如果 PrepareDuration 高,说明 GPU 驱动有额外开销;如果 ExecuteDuration 高,则说明 GPU 实际渲染压力大。
性能稳定性建议
- 每次测量前重启应用,清除缓存避免热点代码被 JIT 干扰。
- 固定测试场景:同一部手机,相同系统版本,相同列表滑动速度(建议用 UiAutomator 录制脚本)。
- 保存原始日志而不是仅保存摘要:后续优化后可在同样脚本上重新解析对比。
经过本章学习,你已经掌握三种测量工具的实操方法,并能建立可复用的性能基线。接下来的章节将基于这些基线进行预加载策略的逐项优化。