Flutter devicelab 内存测试实战:MemoryTest、DevToolsMemoryTest 与 iOS 内存采样三类实现详解
本文基于 Flutter 仓库的官方文档 How to write a memory test for Flutter,系统讲解 Flutter device lab 中当前使用的三类内存测试:直接轮询 ADB 的 MemoryTest、借助 DevTools 的 DevToolsMemoryTest、以及基于 Dart timeline 的 iOS 内存采样测试。读完本文,你将掌握每类测试的底层实现原理、配套的测试应用协议、现有示例任务的完整代码结构,以及将一个新的内存测试用例注册进 Flutter CI 的具体步骤与取舍标准。
内存性能是 Flutter 的高优先级指标,因此仓库中内置了多种内存工具与测试基础设施,且新的内存工具仍在持续演进中,官方文档也注明未来会继续向该文档补充新的测试类别。
一、三类内存测试总览
Flutter devicelab 中的内存测试可分为三大类,分别对应不同的测量通道、运行模式与平台约束:
| 类别 | 测量通道 | 平台 | 可用运行模式 | 测量粒度 | 核心入口 |
|---|---|---|---|---|---|
MemoryTest |
直接轮询 ADB(dumpsys meminfo) |
仅 Android | 全部模式(含 release) | 应用运行前后各 1 次快照 | perf_tests.dart |
DevToolsMemoryTest |
DevTools 轮询 ADB 与 Dart VM | 仅 Android | profile / debug | 约每秒 1 次采样 | perf_tests.dart |
iOS 内存测试(PerfTest + measureMemory) |
iOS 内嵌层运行时采样,写入 Dart timeline | 仅 iOS | profile / debug | 采样频率可调 | perf_tests.dart |
三者的通用结论:MemoryTest 开销最低、支持 release 模式、测试对测量起止有完全控制,但只有 begin/end 两个读数;DevToolsMemoryTest 粒度更细(约每秒一次读数)且额外提供 Dart VM 内存信息,代价是失去 release 模式支持且对测量时机的控制较弱;iOS 采样测试开销最小、采样频率可调,但仅限 iOS 且同样不可用于 release 模式。三类测试的完整 Pros/Cons 在各自小节中逐一给出。
二、第一类:直接轮询 ADB 的 MemoryTest
2.1 工作原理
MemoryTest 类定义在 devicelab 的 perf_tests.dart 中,它会在一个可重写的 useMemory() 函数执行前后,直接轮询 ADB 获取被测应用的内存读数。默认情况下,useMemory() 只做一件事:以 release 模式运行应用,并等待 logcat 中打印出 "done" 消息。
从源码结构看,MemoryTest.run() 的执行流程为:
- 选定一台可用设备并解锁,执行
flutter packages get; - 订阅
device.logcat流,监听形如==== MEMORY BENCHMARK ==== <message> ====的日志行,看到消息时完成对应的Completer(见 logcat 监听逻辑); - 循环执行
iterationCount轮迭代,基类默认iterationCount => 10(见 iterationCount 定义)。每一轮调用一次useMemory(),随后按包名终止应用; - 汇总三轮读数(开始、结束、差值)后,用 ListStatistics 计算每组的 min / max / median,输出
start-min、start-max、start-median、end-*、diff-*共 9 个 benchmark score key。
真正的内存读数来自 AndroidDevice.getMemoryStats:它在设备上执行 dumpsys meminfo <packageName>,用正则 TOTAL\s+(\d+) 解析出 total_kb。这也解释了官方文档列出的两个缺点:ADB 轮询可能触发 Java 堆 GC,且整套机制依赖 device.logcat、device.getMemoryStats 等 Android 专属接口(源码注释明确说明 iOS 尚未实现这些能力),因此该测试只能在安装了 Flutter SDK、能访问 ADB 的主机上对 Android 目标运行。
2.2 测试应用侧的握手协议
MemoryTest 与被测应用之间通过 logcat 中的 ==== MEMORY BENCHMARK ==== <MESSAGE> ==== 协议完成握手,应用需要按约定打印三类消息:
READY:应用启动完成、可以开始测量(launchApp()通过 prepareForNextMessage('READY') 等待);TAPPED:当requiresTapToStart: true时,MemoryTest会以 100ms 间隔持续点击屏幕 (100, 100) 位置,直到应用回TAPPED或超时 10 秒(见 tapNotification),用于确保手势监听器已经装好;DONE:应用完成被测工作负载,useMemory()随即执行recordEnd()快照(见 useMemory 默认实现)。
以 complex_layout 的 test_memory 入口 为例,它先 runApp 一个包裹 ComplexLayoutApp 的 GestureDetector,等一帧后打印 READY;收到 tap 后打印 TAPPED,再移除拦截层、用 LiveWidgetController 对 ListView 做 4 轮下滚 + 4 轮上滚的 fling,最后打印 DONE:
runApp(GestureDetector(
onTap: () {
debugPrint('==== MEMORY BENCHMARK ==== TAPPED ====');
ready.complete();
},
behavior: HitTestBehavior.opaque,
child: const IgnorePointer(child: ComplexLayoutApp()),
));
await SchedulerBinding.instance.endOfFrame;
debugPrint('==== MEMORY BENCHMARK ==== READY ====');
// ...滚动工作负载...
debugPrint('==== MEMORY BENCHMARK ==== DONE ====');
2.3 现有示例任务
| 任务名 | devicelab 任务文件 | 测试应用入口 | 特殊行为 |
|---|---|---|---|
complex_layout_scroll_perf__memory |
complex_layout_scroll_perf__memory.dart | test_memory/scroll_perf.dart | requiresTapToStart: true,包名 com.yourcompany.complexLayout |
fast_scroll_large_images_memory |
fast_scroll_large_images__memory.dart | test_memory/large_images.dart | 重写 iterationCount 为 5,重写 useMemory() 用 input swipe 快速滚动 |
其中 fast_scroll_large_images__memory.dart 展示了如何自定义测量逻辑:它继承 MemoryTest,在 useMemory() 中先 launchApp() + recordStart(),然后通过 device.shellExec('input', ['swipe', '0 1500 0 0 50']) 在 50ms 内把屏幕从底部滑到顶部,等待 15 秒后再 recordEnd()。而应用侧的 large_images.dart 只负责启动 MacrobenchmarksApp 的 kLargeImagesRouteName 路由并打印 READY——滚动动作完全由 host 侧的 ADB 命令驱动。
仓库中还有更多 MemoryTest 用例可作参考,例如 flutter_gallery__memory_nav.dart(Flutter Gallery 导航内存测试,包名 io.flutter.demo.gallery)。
2.4 编写一个新的 MemoryTest 用例
按照官方文档与现有示例,新增一个名为 some_memory_perf 的 MemoryTest 用例需要三步:
- 在测试应用的
test_memory/目录下创建some_memory_perf.dart,其中main()负责按上文协议打印READY/(可选)TAPPED/DONE,并驱动被测工作负载; - 在 devicelab 的 bin/tasks 目录新增同名任务文件(见下文的本地运行方式说明);
- 将
some_memory_perf注册到 devicelab 的任务清单中,使其随每次 Flutter 提交进入 CI 测量(原文档引用的注册入口为dev/devicelab/manifest.yaml;需要注意,在当前仓库树中该 manifest 文件已不存在,任务注册与分片配置已转移到仓库外部的 CI 配置中维护,新增任务时应以仓库当时的 CI 接入流程为准)。
任务文件的最小骨架即现有示例的结构:
Future<void> main() async {
deviceOperatingSystem = DeviceOperatingSystem.android;
await task(
MemoryTest(
'${flutterDirectory.path}/dev/benchmarks/<your_project>', // 测试应用路径
'test_memory/some_memory_perf.dart', // 应用入口
'<package_name>', // Android 包名
// requiresTapToStart: true, // 可选
).run,
);
}
2.5 优缺点小结
官方文档给出的 MemoryTest 权衡:
优点
- 开销低;
- 支持所有运行模式,包括 release;
- 测试完全控制测量何时开始、何时结束。
缺点
- 一次应用运行中只有 begin 和 end 两个内存读数;
- 轮询 ADB 可能触发 Java 堆 GC;
- 仅支持 Android 目标;
- 需要可访问 ADB 的测试环境;
- 需要安装 Flutter SDK 的主机。
三、第二类:基于 DevTools 的 DevToolsMemoryTest
3.1 工作原理
DevToolsMemoryTest 在一次普通的 Flutter driver 测试运行期间,让 DevTools 持续轮询 ADB 与 Dart VM——而 driver 测试通常测的是速度性能,这一类测试把同样的工作负载改造成内存测量。它屏蔽了大部分流程,新测试只需指定 driver 测试的位置。
从 DevToolsMemoryTest.run() 源码看,核心动作就两条命令 + 一段 JSON 解析:
- 执行
flutter drive -d <device> --profile --profile-memory devtools_memory.json --no-publish-port -v <driverTest>(通过 devicelab 的flutter('drive', driveWithDds: true, ...)封装发起); - 读取生成的
devtools_memory.json,遍历samples.data中的每个采样点,取rss字段的最大值得到maxRss,取adb_memoryInfo.Total的最大值得到maxAdbTotal,两者即最终上报的 benchmark score。
这意味着测量粒度约为每秒一次读数(DevTools 的采样节奏),并且同时拿到进程 RSS 与 Dart VM 维度的内存信息。由于走的是 DevTools/DDS 通道,它无法用于 release 模式,在 profile 或 debug 模式下可能引入额外内存开销。
3.2 现有示例任务
| 任务名 | devicelab 任务文件 | 复用的 driver 测试 |
|---|---|---|
complex_layout_scroll_perf__devtools_memory |
complex_layout_scroll_perf__devtools_memory.dart | test_driver/scroll_perf.dart |
large_image_changer_perf_android |
large_image_changer_perf_android.dart | test_driver/large_image_changer.dart |
两个任务文件都只有一行核心调用,例如 large_image_changer_perf_android.dart:
Future<void> main() async {
deviceOperatingSystem = DeviceOperatingSystem.android;
await task(
DevToolsMemoryTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/large_image_changer.dart',
).run,
);
}
注意第一个示例里 driver 测试文件是 scroll_perf.dart——速度测试与内存测试共享同一个 driver 测试文件,这正是"轻松把速度型 driver 测试转化为内存测试"这一优点的具体体现。
3.3 编写步骤与权衡
新增一个 DevTools 内存测试用例 some_memory_perf:
- 编写(或复用)一个普通的 Flutter driver 测试,文件命名如
test_driver/some_memory_perf.dart(应用入口)与test_driver/some_memory_perf_test.dart(测试驱动); - 将
some_memory_perf注册进 devicelab 任务清单(同 2.4 节的说明); - 在 dev/devicelab/bin/tasks 下新增
some_memory_perf.dart,用DevToolsMemoryTest('<app 路径>', '<driver 测试相对路径>')构造任务。
优点
- 测量粒度更细(约每秒 1 个读数);
- 同时获得 Dart VM 内存信息;
- 可以轻松把一个速度型的 driver 测试转成内存测试。
缺点
- 对测量起止时机的控制很弱;
- 轮询 ADB 可能触发 Java 堆 GC;
- 需要可访问 ADB 的测试环境;
- 仅支持 Android 目标;
- 不可用于 release 模式,在 profile / debug 模式下可能带来额外内存开销;
- 需要安装 Flutter SDK 的主机。
四、第三类:iOS 内存测试(timeline 采样)
4.1 工作原理
Flutter 的 iOS 内嵌层支持在运行时对内存使用进行采样,并把指标写入 Dart timeline。对应用执行的相关阶段录制 timeline 后,即可从 profile 中提取内存相关信息。
这一机制落在 devicelab 的 PerfTest 类上:其标准构造函数提供 measureMemory 参数(默认为 true,e2e 变体 PerfTest.e2e 默认为 false,见 measureMemory 字段)。从 得分键的组装逻辑 看,当 measureMemory && !isAndroid 成立且 timeline 中存在对应数据时,任务会把 average_memory_usage、90th_percentile_memory_usage、99th_percentile_memory_usage 纳入 benchmark score。也就是说,iOS driver 测试"免费"获得内存测量——这正是官方文档所说 "Flutter driver tests on iOS get memory measurements for free" 的源码依据。
4.2 现有示例任务
large_image_changer_perf_ios,任务文件 large_image_changer_perf_ios.dart:
Future<void> main() async {
deviceOperatingSystem = DeviceOperatingSystem.ios;
await task(
PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/large_image_changer.dart',
'large_image_changer',
// 该基准不关心帧时间:帧时间受首次加载图片的 IO 影响太大
benchmarkScoreKeys: <String>[
'average_cpu_usage',
'average_gpu_usage',
'average_memory_usage',
'90th_percentile_memory_usage',
'99th_percentile_memory_usage',
'new_gen_gc_count',
'old_gen_gc_count',
],
).run,
);
}
它复用了与 Android 侧完全相同的 driver 测试 test_driver/large_image_changer.dart,仅把运行平台切换为 iOS 并显式声明内存相关得分键(附带 GC 次数与 CPU/GPU 占用)。
4.3 编写步骤与权衡
新增一个 iOS 内存测试用例 some_memory_perf:
- 编写(或复用)普通 Flutter driver 测试,如
test_driver/some_memory_perf.dart与test_driver/some_memory_perf_test.dart; - 注册进 devicelab 任务清单;
- 在 dev/devicelab/bin/tasks 下新增
some_memory_perf.dart,其中PerfTest需显式指定measureMemory: true(标准构造函数虽默认开启,但按文档约定写明意图更清晰)。
优点
- 可以在未安装 Flutter SDK 的机器上运行(采样来自设备端的 iOS 内嵌层);
- 可调节采样频率,读数多与少按需决定;
- 每次采样的开销远小于调用 ADB;
- iOS 上的 Flutter driver 测试免费获得内存测量。
缺点
- 仅支持 iOS 目标;
- 不可用于 release 模式,在 profile / debug 模式下可能有额外内存开销;
- 内存轮询机制本身可能引入额外内存开销。
五、本地运行与验证
三类测试的 devicelab 任务文件都位于 dev/devicelab/bin/tasks 目录。按 dev/devicelab 的 README,在 dev/devicelab 目录下可以用 test runner 按任务名(即任务文件在 bin/tasks 中的基名,例如 complex_layout_scroll_perf__memory)单独运行:
# 在 .../flutter/dev/devicelab 目录下
../../bin/cache/dart-sdk/bin/dart bin/test_runner.dart test -t {NAME_OF_TEST}
运行多个任务可重复 -t 选项;加 --exit 可跳过内置的失败自动重试(默认失败会重试 2 次)。若要对照本地引擎构建验证内存行为,README 中还给出了 --local-engine-src-path / --local-engine / --local-engine-host 参数组合。
六、选型建议
综合三类测试的源码实现与文档权衡,选型可以按以下优先级考虑:
- 需要 release 模式数据、或需要精确控制测量窗口(例如测量某个工作负载结束后的驻留内存):选
MemoryTest,代价是只有两个读数; - 需要时间序列曲线、或想知道 Dart VM 维度内存:选
DevToolsMemoryTest,直接复用既有 driver 测试文件即可,但要接受 profile 模式下的测量偏差; - iOS 平台、或希望最小化测量开销:用
PerfTest+measureMemory: true,内存数据从 timeline 采样中解析,采样频率可调。
最后需要说明适用前提:以上所有测量都面向 Flutter 的 CI 基础设施(device lab),需要真实的 Android / iOS 设备与相应 SDK 环境;原文档也提示,内存性能工具仍在快速演进,未来会有新的内存测试工具与测试工具类补充进官方文档,实际贡献时应以仓库当时的最新工具为准。
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