第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 gfxinfoGPU渲染流水线读取GPU帧缓冲队列的详细渲染阶段耗时(Draw/Prepare/Process/Execute)Total frame time vs. 16.67ms budget / overdraw
列表滑动性能测量三大工具协同流程图 列表滑动性能测量流程 ① 应用冷启动 ② BaselineProfile ③ FrameTiming ④ dumpsys gfxinfo ⑤ 原始数据汇聚(基线JSON/CSV) Δ 对比优化前后 每次测量后立即保存输出,避免手动记录遗漏

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 录制脚本)。
  • 保存原始日志而不是仅保存摘要:后续优化后可在同样脚本上重新解析对比。

经过本章学习,你已经掌握三种测量工具的实操方法,并能建立可复用的性能基线。接下来的章节将基于这些基线进行预加载策略的逐项优化。