Flutter 滚动与启动性能基准实测:complex_layout 基准套件使用与源码解析
本文以 Flutter 仓库中的 dev/benchmarks/complex_layout 基准套件为核心,讲解如何在一台真机上运行「滚动性能基准」与「启动性能基准」两条命令,并深入剖析该基准 App 的布局结构、driver 测试如何采集 Timeline 数据、坏滚动基线(bad scroll)与语义树基准等配套设计,帮助读者不仅会跑基准,还能理解每个指标背后的测量原理与数据产出位置。
基准套件概览:一个模拟社交信息流的复杂布局
complex_layout 是 Flutter 仓库自带的一个性能基准工程,其 pubspec.yaml 中对它的描述是 "A benchmark of a relatively complex layout"(一个相对复杂布局的基准测试)。它依赖 flutter、flutter_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.dart。ComplexLayoutApp 是一个 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叠加编辑/缩放IconButton和Positioned水印)+InfoBar+Divider+IconBar+FatDivider。 - FancyGalleryItem(奇数项):
UserHeader+ItemGalleryBox(200px 高的DefaultTabController包裹 4 页TabBarView,每页含PageStorageKey、Card与IconButton)+ 其余信息条。
页面骨架还包括:带 TopBarMenu(10 项 PopupMenuButton)的 AppBar、底部固定的 BottomBar(5 个按钮),以及整体 Scaffold + Column 布局。正是这种图片、富文本、Tab、阴影(Material.elevation)、分隔线混合的场景,让它在滚动时对 build/layout/paint/raster 各环节都有真实的负载。
另外两个值得注意的设计:
timeDilation慢放:toggleAnimationSpeed()会把全局timeDilation在 1.0 与 5.0 之间切换(见 lib/src/app.dart),抽屉里提供 "Animate Slowly" 开关,方便肉眼观察动画与滚动细节。badScroll坏滚动模式:入口 lib/main_bad.dart 以runApp(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.dart 是 flutter 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 做滚动基准"的参考模板:
- 等待首帧光栅化:
driver.waitUntilFirstFrameRasterized(),确保测量从稳定状态开始。 - 降噪延迟:测量前
await Future.delayed(250ms),源码注释解释这是为了避免在设备负载较高的初始阶段开始计时("Without this delay, the benchmark has greater noise")。 - 强制 GC:
driver.forceGC(),排除垃圾回收对帧时间的干扰。 - 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 所述两个产物一一对应:summaryName为complex_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.dart 以 main_bad 入口启动(即 badScroll: true 的坏布局版本),配套的 test_driver/scroll_perf_bad_test.dart 只测 complex-scroll-bad 这一个 key,但滚动逻辑与正向基准完全一致。可以推断,这套"坏滚动"数据在 CI 中用作回归下限参考:正常实现的性能如果退化到接近这个反模式水平,就应当被拦截。这也是为什么 lib/src/app.dart 中 badScroll 分支同时改动 itemCount、shrinkWrap 并嵌套外层 ListView 三个地方——刻意制造可测量的性能劣化。
语义树性能基准:SEMANTICS 事件的隔离测量
滚动基准之外,该套件还包含一个测量无障碍语义树构建成本的用例 test_driver/semantics_perf_test.dart,其 driver 入口是 test_driver/semantics_perf.dart(结构与 scroll_perf.dart 相同,同样调用 enableFlutterDriverExtension() 后启动应用)。它的关键设计:
- 测试前通过 adb 关闭系统无障碍服务(
settings put secure enabled_accessibility_services null),源码注释解释:若系统无障碍服务开着,语义树会在首帧就生成,无法单独测量"初次生成"耗时; - 等应用完全 idle 2 秒后
forceGC(); - 用
driver.traceAction包裹driver.setSemantics(true),把"打开语义"这一动作圈进 Timeline; - 从 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 序列),并通过 ResampleFlagVariant 对 binding.resamplingEnabled 开/关 × 两种输入频率共 4 种组合分别测量。
平滑度用滚动偏移的离散二阶导数绝对值(abs_jerk = |f[i-1] + f[i+1] - 2*f[i]|)度量。文件中的注释解释了选型理由:输入是纯线性的,理想加速度严格为 0,因此二阶导偏离即为抖动;用绝对值而非平方,是为了避免大值数据点过度放大权重。同时会过滤掉"帧构建超过 40ms"或"输入模拟延迟超过 16ms(1/60Hz)"的帧,把构建过慢与响应延迟导致的卡顿与真正的抖动分离。输出 map 包含 average_abs_jerk(越小越平滑)、janky_count(abs_jerk > 0.5 的帧数)、dropped_frame_count 以及逐帧的 frame_timestamp、scroll_offset、input_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 filebuild/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.json、build/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.json(initialSemanticsTreeCreation 毫秒数) |
| 启动时间 | 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: workspace与sdk: ^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 滚动内存基准脚本 |
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00