首页
/ Flutter 滚动与启动性能基准实测:complex_layout 基准套件使用与源码解析

Flutter 滚动与启动性能基准实测:complex_layout 基准套件使用与源码解析

2026-09-04 18:36:39作者:劳婵绚Shirley

本文以 Flutter 仓库中的 dev/benchmarks/complex_layout 基准套件为核心,讲解如何在一台真机上运行「滚动性能基准」与「启动性能基准」两条命令,并深入剖析该基准 App 的布局结构、driver 测试如何采集 Timeline 数据、坏滚动基线(bad scroll)与语义树基准等配套设计,帮助读者不仅会跑基准,还能理解每个指标背后的测量原理与数据产出位置。

基准套件概览:一个模拟社交信息流的复杂布局

complex_layout 是 Flutter 仓库自带的一个性能基准工程,其 pubspec.yaml 中对它的描述是 "A benchmark of a relatively complex layout"(一个相对复杂布局的基准测试)。它依赖 flutterflutter_driver(均以 SDK 方式引入)以及外部图片资源包 flutter_gallery_assets,其中两张图片被显式声明为资产:

flutter:
  uses-material-design: true
  assets:
    - packages/flutter_gallery_assets/people/square/ali.png
    - packages/flutter_gallery_assets/places/india_chettinad_silk_maker.png

应用入口 lib/main.dart 只做一件事——运行 ComplexLayoutApp

void main() {
  runApp(const ComplexLayoutApp());
}

真正的内容在 lib/src/app.dartComplexLayoutApp 是一个 StatefulWidget,内部维护两个可切换状态:

  • 滚动模式ScrollMode 枚举,取值 complex / tile):默认渲染 ComplexLayout 页面;切换到 tile 后渲染 TileScrollLayout——一个只有 200 个简单 IconBar 条目的 ListView.builder(key 为 tiles-scroll),作为低负载对照组。
  • 主题lightTheme 布尔值):切换 ThemeData()ThemeData.dark()

这两者都通过侧边抽屉(GalleryDrawer)中的 ListTile 切换。其中滚动模式切换项带有 Key('scroll-switcher'),这个 key 会被 driver 测试用来程序化切换对照组。

被测页面:为什么这个布局"复杂"

ComplexLayout 页面模拟了一个社交信息流,主体是带 Key('complex-scroll')ListView.builder(源码注释明确写道该 key 供 driver 测试使用),并持有自己的 ScrollController 以便跟踪滚动偏移量。每一项按索引奇偶交替渲染两种组件:

Widget body = ListView.builder(
  key: const Key('complex-scroll'), // this key is used by the driver test
  controller: ScrollController(), // So that the scroll offset can be tracked
  itemCount: widget.badScroll ? 500 : null,
  shrinkWrap: widget.badScroll,
  itemBuilder: (BuildContext context, int index) {
    if (index.isEven) {
      return FancyImageItem(index, key: PageStorageKey<int>(index));
    } else {
      return FancyGalleryItem(index, key: PageStorageKey<int>(index));
    }
  },
);

两种条目都通过 ListBody 组合了多层嵌套 Widget,覆盖了大量实际渲染路径:

  • FancyImageItem(偶数项):UserHeader(头像 AssetImage + RichText 富文本)+ ItemDescription(长文本段落)+ ItemImageBox(一张 230px 高的图片,Stack 叠加编辑/缩放 IconButtonPositioned 水印)+ InfoBar + Divider + IconBar + FatDivider
  • FancyGalleryItem(奇数项):UserHeader + ItemGalleryBox(200px 高的 DefaultTabController 包裹 4 页 TabBarView,每页含 PageStorageKeyCardIconButton)+ 其余信息条。

页面骨架还包括:带 TopBarMenu(10 项 PopupMenuButton)的 AppBar、底部固定的 BottomBar(5 个按钮),以及整体 Scaffold + Column 布局。正是这种图片、富文本、Tab、阴影(Material.elevation)、分隔线混合的场景,让它在滚动时对 build/layout/paint/raster 各环节都有真实的负载。

另外两个值得注意的设计:

  1. timeDilation 慢放toggleAnimationSpeed() 会把全局 timeDilation 在 1.0 与 5.0 之间切换(见 lib/src/app.dart),抽屉里提供 "Animate Slowly" 开关,方便肉眼观察动画与滚动细节。
  2. badScroll 坏滚动模式:入口 lib/main_bad.dartrunApp(const ComplexLayoutApp(badScroll: true)) 启动。启用后列表固定 500 项、开启 shrinkWrap,并且把懒加载的 ListView.builder 包进一个外层 ListView(key 为 complex-scroll-bad)——即"列表套列表、一次构建全部"的反模式,用于回归检测(后文详述)。

滚动性能基准:运行与产物

README 给出的原始操作如下(在 dev/benchmarks/complex_layout 目录下,设备已连接时):

flutter drive --profile test_driver/scroll_perf.dart
  • 结果文件build/complex_layout_scroll_perf.timeline_summary.json(汇总指标)。
  • 详细日志build/complex_layout_scroll_perf.timeline.json(完整 Timeline 事件流,可用于事后分析)。

driver 入口:在 App 内启用测试扩展

test_driver/scroll_perf.dartflutter drive 指定的入口,它先启用 driver 扩展再启动应用:

import 'package:complex_layout/main.dart' as app;
import 'package:flutter_driver/driver_extension.dart';

void main() {
  enableFlutterDriverExtension();
  app.main();
}

enableFlutterDriverExtension() 让运行在设备上的应用可以响应 host 侧 FlutterDriver 发来的滚动、点击、GC、timeline 采集等指令——这是整个滚动基准能工作的前提。

测量脚本:先稳定、再 trace、后汇总

真正执行测量的是 test_driver/scroll_perf_test.dart,其流程可直接作为"用 flutter_driver 做滚动基准"的参考模板:

  1. 等待首帧光栅化driver.waitUntilFirstFrameRasterized(),确保测量从稳定状态开始。
  2. 降噪延迟:测量前 await Future.delayed(250ms),源码注释解释这是为了避免在设备负载较高的初始阶段开始计时("Without this delay, the benchmark has greater noise")。
  3. 强制 GCdriver.forceGC(),排除垃圾回收对帧时间的干扰。
  4. traceAction 包裹滚动动作
final Timeline timeline = await driver.traceAction(() async {
  final SerializableFinder list = find.byValueKey(listKey);
  expect(list, isNotNull);

  // Scroll down
  for (var i = 0; i < 5; i += 1) {
    await driver.scroll(list, 0.0, -300.0, const Duration(milliseconds: 300));
    await Future<void>.delayed(const Duration(milliseconds: 500));
  }
  // Scroll up
  for (var i = 0; i < 5; i += 1) {
    await driver.scroll(list, 0.0, 300.0, const Duration(milliseconds: 300));
    await Future<void>.delayed(const Duration(milliseconds: 500));
  }
}, retainPriorEvents: true);

final summary = TimelineSummary.summarize(timeline);
await summary.writeTimelineToFile(summaryName, pretty: true);

要点:

  • 通过 find.byValueKey(listKey) 定位可滚动组件——这正是页面上 Key('complex-scroll') / Key('tiles-scroll') 存在的意义;
  • 每次滚动 300px、耗时 300ms,之后停顿 500ms,让惯性滚动(fling)自然衰减;共 5 次向下 + 5 次向上,形成对称的完整滚动周期;
  • traceAction(retainPriorEvents: true) 让 Timeline 同时保留动作之前的事件;
  • TimelineSummary.summarize 把原始事件流压缩为汇总,writeTimelineToFile 落盘。这与 README 所述两个产物一一对应:summaryNamecomplex_layout_scroll_perf 时,生成 complex_layout_scroll_perf.timeline_summary.json(汇总)与 complex_layout_scroll_perf.timeline.json(明细)。

同一个测试文件里还包含第二条用例 tiles_scroll_perf:它先 driver.tap(find.byTooltip('Open navigation menu')) 打开抽屉,再 driver.tap(find.byValueKey('scroll-switcher')) 切到 Tile 模式,然后对 tiles-scroll 列表重复同样的滚动测量。也就是说,一次 flutter drive 会同时产出"复杂布局"与"简单 Tile 对照组"两套基准数据。

bad scroll 基线:为回归检测服务的"已知慢实现"

test_driver/scroll_perf_bad.dartmain_bad 入口启动(即 badScroll: true 的坏布局版本),配套的 test_driver/scroll_perf_bad_test.dart 只测 complex-scroll-bad 这一个 key,但滚动逻辑与正向基准完全一致。可以推断,这套"坏滚动"数据在 CI 中用作回归下限参考:正常实现的性能如果退化到接近这个反模式水平,就应当被拦截。这也是为什么 lib/src/app.dartbadScroll 分支同时改动 itemCountshrinkWrap 并嵌套外层 ListView 三个地方——刻意制造可测量的性能劣化。

语义树性能基准:SEMANTICS 事件的隔离测量

滚动基准之外,该套件还包含一个测量无障碍语义树构建成本的用例 test_driver/semantics_perf_test.dart,其 driver 入口是 test_driver/semantics_perf.dart(结构与 scroll_perf.dart 相同,同样调用 enableFlutterDriverExtension() 后启动应用)。它的关键设计:

  1. 测试前通过 adb 关闭系统无障碍服务settings put secure enabled_accessibility_services null),源码注释解释:若系统无障碍服务开着,语义树会在首帧就生成,无法单独测量"初次生成"耗时;
  2. 等应用完全 idle 2 秒后 forceGC()
  3. driver.traceAction 包裹 driver.setSemantics(true),把"打开语义"这一动作圈进 Timeline;
  4. 从 Timeline 事件中提取所有 SEMANTICS 事件,断言恰好 2 条,用末条与首条的时间戳差值算出 initialSemanticsTreeCreation(毫秒),写入 testOutputsDirectory/complex_layout_semantics_perf.json
final String jsonEncoded = json.encode(<String, dynamic>{
  'initialSemanticsTreeCreation': semanticsTreeCreation.inMilliseconds,
});

对做无障碍支持或语义优化的人来说,这是一个把"语义树首次构建耗时"变成可追踪数字的实用范例。

平滑度基准:abs_jerk 指标的实现

test/measure_scroll_smoothness.dart 是另一个方向:不关心单帧耗时,而关心滚动是否线性平滑。它基于 integration_test 在真机上模拟 90Hz/59Hz 的指针拖拽事件(dragInputEvents 按频率生成 PointerMoveEvent 序列),并通过 ResampleFlagVariantbinding.resamplingEnabled 开/关 × 两种输入频率共 4 种组合分别测量。

平滑度用滚动偏移的离散二阶导数绝对值abs_jerk = |f[i-1] + f[i+1] - 2*f[i]|)度量。文件中的注释解释了选型理由:输入是纯线性的,理想加速度严格为 0,因此二阶导偏离即为抖动;用绝对值而非平方,是为了避免大值数据点过度放大权重。同时会过滤掉"帧构建超过 40ms"或"输入模拟延迟超过 16ms(1/60Hz)"的帧,把构建过慢与响应延迟导致的卡顿与真正的抖动分离。输出 map 包含 average_abs_jerk(越小越平滑)、janky_countabs_jerk > 0.5 的帧数)、dropped_frame_count 以及逐帧的 frame_timestampscroll_offsetinput_delay 明细。

启动性能基准:--trace-startup

README 的第二条命令测量的是启动时间,而不是滚动:

flutter run --profile --trace-startup
  • --profile:以 Profile 模式(AOT 编译、关闭调试断言)启动,使启动数据贴近真实发布形态;
  • --trace-startup:开启启动跟踪,各启动阶段耗时会打印在运行日志中("The results should be in the logs");
  • 更结构化的启动数据会写入 build/start_up_info.json("Additional results should be in the file build/start_up_info.json")。

由于 complex_layout 的首页本身就包含图片解码、多层嵌套布局与 Tab 控件,用它跑 --trace-startup 得到的是"打开一个带复杂首屏的 Flutter 应用要多久"的数据,而非空壳页面的启动数据。

配套:滚动内存基准

test_memory/scroll_perf.dart 是供 devicelab 内存类任务调用的变体:它先显示一个等待层(打印 ==== MEMORY BENCHMARK ==== READY ====),等设备侧任务发出同步点击后,移除等待层并启用真实指针事件,然后用 LiveWidgetController.fling 做 4 次向下甩动(每次 -700px、末速 1500px/s、间隔 500ms)+ 4 次向上甩动,最后打印 ==== MEMORY BENCHMARK ==== DONE ====。它复用同一套 ComplexLayoutApp 布局,让内存采样发生在真实的滚动加载过程中——懒加载列表边滚边构建,才能观察到滚动引起的内存增长。

实操小结

场景 命令(在 dev/benchmarks/complex_layout 下,设备已连接) 产物
滚动性能(复杂 + Tile 对照) flutter drive --profile test_driver/scroll_perf.dart build/complex_layout_scroll_perf.timeline_summary.jsonbuild/complex_layout_scroll_perf.timeline.json(含 tiles_scroll_perf 对应文件)
坏滚动基线 flutter drive --profile test_driver/scroll_perf_bad.dart 同为 complex_layout_scroll_perf 命名前缀的 timeline 文件
语义树构建耗时 flutter drive --profile test_driver/semantics_perf.dart complex_layout_semantics_perf.jsoninitialSemanticsTreeCreation 毫秒数)
启动时间 flutter run --profile --trace-startup 运行日志 + build/start_up_info.json
平滑度(abs_jerk) flutter drive -t test/measure_scroll_smoothness.dart ...(integration_test 方式运行) binding.reportData 中 4 种场景的 average_abs_jerk

几个前提与限制值得注意:

  • 所有 drive 类基准需要真机(driver 走 VM Service 与 timeline 采集),且 App 必须先以 profile 模式构建;
  • 滚动基准的 driver 脚本硬编码了滚动距离(300px/次)、次数(5+5)与停顿(500ms),换用别的设备分辨率时数值口径会不同,横向对比应在同一设备上进行;
  • semantics_perf 会主动修改设备的无障碍设置(测试前关闭),仅建议在专用测试机上运行;
  • 工程 pubspec.yaml 声明 resolution: workspacesdk: ^3.11.0-0,说明它运行在 Flutter 仓库自身的工作区解析体系内,flutter_gallery_assets 等依赖由仓库根工作区提供,脱离仓库单独 checkout 可能无法直接解析依赖。

关键文件索引

文件 作用
dev/benchmarks/complex_layout/README.md 基准运行说明(本文依据的原始文档)
dev/benchmarks/complex_layout/lib/src/app.dart 被测布局:ComplexLayoutApp、两种滚动模式、badScroll 分支、抽屉控制项
dev/benchmarks/complex_layout/test_driver/scroll_perf.dart / scroll_perf_test.dart 滚动基准的 driver 入口与测量脚本(traceAction、forceGC、TimelineSummary)
dev/benchmarks/complex_layout/test_driver/scroll_perf_bad.dart / scroll_perf_bad_test.dart 坏滚动基线的入口与测量脚本
dev/benchmarks/complex_layout/test_driver/semantics_perf_test.dart 语义树首次构建耗时测量
dev/benchmarks/complex_layout/test/measure_scroll_smoothness.dart 平滑度(abs_jerk)指标实现
dev/benchmarks/complex_layout/test_memory/scroll_perf.dart devicelab 滚动内存基准脚本
登录后查看全文
热门项目推荐
相关项目推荐