首页
/ Flutter devicelab 内存测试实战:MemoryTest、DevToolsMemoryTest 与 iOS 内存采样三类实现详解

Flutter devicelab 内存测试实战:MemoryTest、DevToolsMemoryTest 与 iOS 内存采样三类实现详解

2026-09-06 13:19:26作者:吴年前Myrtle

本文基于 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() 的执行流程为:

  1. 选定一台可用设备并解锁,执行 flutter packages get
  2. 订阅 device.logcat 流,监听形如 ==== MEMORY BENCHMARK ==== <message> ==== 的日志行,看到消息时完成对应的 Completer(见 logcat 监听逻辑);
  3. 循环执行 iterationCount 轮迭代,基类默认 iterationCount => 10(见 iterationCount 定义)。每一轮调用一次 useMemory(),随后按包名终止应用;
  4. 汇总三轮读数(开始、结束、差值)后,用 ListStatistics 计算每组的 min / max / median,输出 start-minstart-maxstart-medianend-*diff-* 共 9 个 benchmark score key。

真正的内存读数来自 AndroidDevice.getMemoryStats:它在设备上执行 dumpsys meminfo <packageName>,用正则 TOTAL\s+(\d+) 解析出 total_kb。这也解释了官方文档列出的两个缺点:ADB 轮询可能触发 Java 堆 GC,且整套机制依赖 device.logcatdevice.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 一个包裹 ComplexLayoutAppGestureDetector,等一帧后打印 READY;收到 tap 后打印 TAPPED,再移除拦截层、用 LiveWidgetControllerListView 做 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 只负责启动 MacrobenchmarksAppkLargeImagesRouteName 路由并打印 READY——滚动动作完全由 host 侧的 ADB 命令驱动。

仓库中还有更多 MemoryTest 用例可作参考,例如 flutter_gallery__memory_nav.dart(Flutter Gallery 导航内存测试,包名 io.flutter.demo.gallery)。

2.4 编写一个新的 MemoryTest 用例

按照官方文档与现有示例,新增一个名为 some_memory_perfMemoryTest 用例需要三步:

  1. 在测试应用的 test_memory/ 目录下创建 some_memory_perf.dart,其中 main() 负责按上文协议打印 READY /(可选)TAPPED / DONE,并驱动被测工作负载;
  2. 在 devicelab 的 bin/tasks 目录新增同名任务文件(见下文的本地运行方式说明);
  3. 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 解析:

  1. 执行 flutter drive -d <device> --profile --profile-memory devtools_memory.json --no-publish-port -v <driverTest>(通过 devicelab 的 flutter('drive', driveWithDds: true, ...) 封装发起);
  2. 读取生成的 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

  1. 编写(或复用)一个普通的 Flutter driver 测试,文件命名如 test_driver/some_memory_perf.dart(应用入口)与 test_driver/some_memory_perf_test.dart(测试驱动);
  2. some_memory_perf 注册进 devicelab 任务清单(同 2.4 节的说明);
  3. 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_usage90th_percentile_memory_usage99th_percentile_memory_usage 纳入 benchmark score。也就是说,iOS driver 测试"免费"获得内存测量——这正是官方文档所说 "Flutter driver tests on iOS get memory measurements for free" 的源码依据。

4.2 现有示例任务

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

  1. 编写(或复用)普通 Flutter driver 测试,如 test_driver/some_memory_perf.darttest_driver/some_memory_perf_test.dart
  2. 注册进 devicelab 任务清单;
  3. 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 参数组合。

六、选型建议

综合三类测试的源码实现与文档权衡,选型可以按以下优先级考虑:

  1. 需要 release 模式数据、或需要精确控制测量窗口(例如测量某个工作负载结束后的驻留内存):选 MemoryTest,代价是只有两个读数;
  2. 需要时间序列曲线、或想知道 Dart VM 维度内存:选 DevToolsMemoryTest,直接复用既有 driver 测试文件即可,但要接受 profile 模式下的测量偏差;
  3. iOS 平台、或希望最小化测量开销:用 PerfTest + measureMemory: true,内存数据从 timeline 采样中解析,采样频率可调。

最后需要说明适用前提:以上所有测量都面向 Flutter 的 CI 基础设施(device lab),需要真实的 Android / iOS 设备与相应 SDK 环境;原文档也提示,内存性能工具仍在快速演进,未来会有新的内存测试工具与测试工具类补充进官方文档,实际贡献时应以仓库当时的最新工具为准。

登录后查看全文
热门项目推荐
相关项目推荐