☰
Flutter DeviceLab 实战指南:在真实设备上运行 Flutter 性能与正确性测试
2026/10/14 3:14:53 网站建设 项目流程
  • 跨平台
  • 前端
  • 移动开发
  • OpenHarmony

【免费下载链接】flutter_flutter

本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。

项目地址:https://gitcode.com/CPF-Flutter/flutter_flutter
点击查看免费下载

DeviceLab 是 Flutter 官方的物理设备测试实验室,用于在真实手机上验证 Flutter 的正确性与性能。本指南以flutter_flutter仓库中 dev/devicelab/README.md 为核心骨架,结合仓库源码与测试用例,系统讲解 DeviceLab 的任务调度模型、本地运行方法、测试编写规范、CI 接入方式以及本仓库在 OpenHarmony 平台上的适配细节。读完本文,你将能够在本地复现 CI 失败、编写自己的 DeviceLab 任务、运行引擎 A/B 性能对比测试,并理解如何把新测试接入持续集成。

DeviceLab 是什么:物理设备上的测试框架

DeviceLab 是一套在真实物理设备上测试 Flutter 的框架。本仓库中的 dev/devicelab 目录同时包含两样东西:

  • 测试框架本身的代码:位于 dev/devicelab/lib 下,包名为flutter_devicelab(见 dev/devicelab/pubspec.yaml);
  • 具体测试:位于 dev/devicelab/bin/tasks 下,每个.dart文件就是一个独立测试。

在框架 API 中,这些测试统称为task(任务)。由于这套框架主要用于测试,因此文档统一称之为"测试"。

每个任务都会声明自己需要运行在哪种类型的设备上(如linux_android、mac_ios、mac_android、windows_android等)。当实验室中某台设备空闲时,调度系统就会把匹配的任务分配给这台设备执行。当前各任务的运行状态可以通过 Cocoon 基础设施提供的 Flutter 仪表盘查看(README 中给出了构建状态入口),仪表盘的使用方式记录在 Cocoon 项目的 dashboard 用户指南中。

任务执行的三种结局:成功、重试与失败上报

DeviceLab 的任务执行结果由测试 runner 统一处理,逻辑实现在 dev/devicelab/lib/framework/runner.dart 的runTasks/rerunTask/runTask中,规则如下:

  1. 任务成功:runner 上报成功,并将该任务记录的性能指标上传到 Flutter 基础设施。注意,并非所有任务都会记录性能指标——只有通过benchmarkScoreKeys声明了评分键的任务才会产生可上报的指标(见 dev/devicelab/lib/framework/task_result.dart 中TaskResult.success对benchmarkScoreKeys的校验逻辑)。
  2. 任务失败后自动重跑:每当最后一次运行成功,任务最终仍会被记为成功,但同时会标记一个flake(不稳定)标志并写入测试结果。源码中重试次数由 dev/devicelab/lib/framework/metrics_result_writer.dart 的MetricsResultWriter.retryNumber = 2决定,即默认最多自动重试 2 次;runner 在failureCount > 0但最终成功时会打印flaky: true。
  3. 所有重试均失败:runner 将失败上报给 Flutter 基础设施,此时不会收集任何性能指标,最终结果记为flaky: false且进程退出码为 1。

此外,在 CI 场景下 runner 会通过--results-file将每次任务的执行结果写入文件,供 Cocoon 的MetricsResultWriter落盘,便于事后审计与回放。

本地运行 DeviceLab 测试

在部署到 CI 环境之前,务必先在本地确认测试可以通过。下面的命令与 CI 的运行方式高度一致,也常用于本地复现 CI 失败。

前置条件:Android 测试需要ANDROID_SDK_ROOT

要在本地运行 Android 测试,必须先设置环境变量ANDROID_SDK_ROOT。如果你构建过本地 Flutter engine,那么 SDK 通常位于.../engine/src/third_party/android_tools/sdk。也可以通过flutter doctor -v查到你机器上 Android SDK 的安装位置。

运行前的环境警告

在本地运行 DeviceLab 测试会对你的环境产生影响,请务必知晓:

  • 它会自动在你的机器上启动和停止 Gradle;
  • 当目标 Android 或 iOS 设备累计运行一定数量的测试后,runner 会自动重启设备,之后才继续运行后续测试。重启逻辑见 dev/devicelab/lib/framework/framework.dart 中的checkForRebootRequired()与 dev/devicelab/lib/framework/devices.dart 中的device.reboot()。

从源码看,重启阈值由framework.dart顶部的常量maximumRuns = 30控制:runner 会在设备主目录(Linux/macOS 为$HOME,Windows 为%USERPROFILE%)维护一个.reboot-count文件,每跑完一个任务计数 +1,达到 30 次后删除计数文件并触发device.reboot()。同时存在一个noRebootForbidList名单,名单内的设备(例如需要人工干预才能重启的 32 位 iPhone)会被排除在自动重启之外。

运行框架单元测试(test/...目录)

dev/devicelab/test 目录下存放的是框架自身的单元测试(例如 runner、AB 测试、metrics 写入器、hdc 适配层的测试),可以通过标准 Dart 测试命令运行:

dart test test/{NAME_OF_TEST}

运行具体任务:-t/--task

在dev/devicelab目录下,使用test_runner.dart的test子命令运行单个任务:

# 从 .../flutter/dev/devicelab 目录执行 ../../bin/cache/dart-sdk/bin/dart bin/test_runner.dart test -t {NAME_OF_TEST}

其中NAME_OF_TEST是任务的名称,也就是bin/tasks下某个文件的文件名(不含 .dart 后缀),例如complex_layout__start_up。也可以在根目录通过bin/run.dart运行:

../../bin/cache/dart-sdk/bin/dart bin/run.dart -t {NAME_OF_TEST}

要一次运行多个任务,只需重复-t(--task)选项:

../../bin/cache/dart-sdk/bin/dart bin/run.dart -t test1 -t test2 -t test3

跳过自动重试:--exit

默认情况下 DeviceLab 测试内置自动重试逻辑,失败的任务会被额外重试 2 次。通过--exit选项可以跳过重试、在第一次失败时立即退出:

../../bin/cache/dart-sdk/bin/dart bin/test_runner.dart test --exit -t {NAME_OF_TEST}

针对本地引擎构建运行测试

如果希望让测试基于本地构建的 Flutter engine 运行,向bin/run.dart传入对应参数即可:

../../bin/cache/dart-sdk/bin/dart bin/run.dart --task=[some_task] \ --local-engine-src-path=[path_to_local]/engine/src \ --local-engine=[local_engine_architecture] \ --local-engine-host=[local_engine_host_architecture]
  • --local-engine-src-path:本地 engine 的src/目录路径。默认取$FLUTTER_ENGINE环境变量,或在未设置时根据--flutter-root猜测位置。
  • --local-engine:engine out 目录下的某个构建产物名称,例如android_debug_unopt_x86。
  • --local-engine-host:作为宿主平台使用的引擎构建名称,例如host_debug_unopt。

这些参数在 dev/devicelab/bin/run.dart 的createArgParser中均有完整定义,会被以-DlocalEngine=...、-DlocalEngineHost=...、-DlocalEngineSrcPath=...的形式透传给任务的 Dart VM(见 dev/devicelab/lib/framework/runner.dart 的runTask)。

引擎改动 A/B 性能对比测试

A/B 测试用于对比默认引擎与本地引擎构建的性能差异:runner 会对同一个 benchmark 在两种引擎下各运行指定次数,然后输出一个以 Tab 分隔的结果表,并将结果存入 JSON 文件以便日后复现;结果可拷贝到电子表格进一步分析,JSON 文件则可用summarize.dart命令做更详细的汇总输出。

../../bin/cache/dart-sdk/bin/dart bin/run.dart --ab=10 \ --local-engine=host_debug_unopt \ --local-engine-host=host_debug_unopt \ -t bin/tasks/web_benchmarks_canvaskit.dart

参数含义:

  • --ab=10:A/B 测试共运行 10 次(每组各 10 次)。--ab的取值必须是数字,否则run.dart会直接抛错。
  • --local-engine=host_debug_unopt:指定参与对比的本地引擎构建。A/B 模式必须指定--local-engine(移动端场景还必须指定--local-engine-host),否则 runner 会拒绝启动。
  • --local-engine-host=host_debug_unopt:用于运行frontend_server(此例中)的同名引擎构建。
  • -t:指定唯一任务。A/B 模式只支持运行恰好一个任务,传入多个任务会报错退出(校验见run.dart的_runABTest前置检查)。

--ab-result-file=filename可指定结果 JSON 文件的输出位置,默认值为ABresults#.json。文件名中的单个#字符表示"若该名称的文件已存在,则在此处插入递增序号";不含#时同名文件会被直接覆盖(去重逻辑见run.dart中的_uniqueFile)。

示例输出(Tab 分隔):

Score Average A (noise) Average B (noise) Speed-up bench_card_infinite_scroll.canvaskit.drawFrameDuration.average 2900.20 (8.44%) 2426.70 (8.94%) 1.20x bench_card_infinite_scroll.canvaskit.totalUiFrame.average 4964.00 (6.29%) 4098.00 (8.03%) 1.21x draw_rect.canvaskit.windowRenderDuration.average 1959.45 (16.56%) 2286.65 (0.61%) 0.86x draw_rect.canvaskit.sceneBuildDuration.average 1969.45 (16.37%) 2294.90 (0.58%) 0.86x draw_rect.canvaskit.drawFrameDuration.average 5335.20 (17.59%) 6437.60 (0.59%) 0.83x draw_rect.canvaskit.totalUiFrame.average 6832.00 (13.16%) 7932.00 (0.34%) 0.86x

输出包含每个指标的平均值与噪声,其中噪声定义为总体标准差除以平均值(计算实现见 dev/devicelab/lib/framework/ab.dart 的_computeStandardDeviationForPopulation与_ScoreSummary)。最关键的是Speed-up列,它表示本地引擎相对默认引擎快了多少倍:小于 1.0 表示变慢,例如0.5x表示本地引擎比默认引擎慢一倍,2.0x表示快一倍,数值越大越好。

用summarize.dart汇总多份结果

对 A/B 生成的 JSON 结果文件做进一步汇总处理:

../../bin/cache/dart-sdk/bin/dart bin/summarize.dart --[no-]tsv-table --[no-]raw-summary \ ABresults.json ABresults1.json ABresults2.json ...
  • --[no-]tsv-table:以 Tab 分隔的表格形式打印汇总,便于直接粘贴进电子表格(默认开启);
  • --[no-]raw-summary:以 Tab 分隔打印 A/B 测试采集到的全部逐次运行数据(默认开启)。

命令行末尾可跟多个结果文件名,每个文件都会被依次处理。另外,dev/devicelab/bin/summarize.dart 的源码中还提供了第三个选项--[no-]ascii-table(默认开启),用于以适合终端展示的对齐表格输出汇总。

本地复现 CI 构建失败

当 CI 构建失败时,可以git checkout到对应的 Flutter revision 在本地复现问题。注意记录失败测试的名称——例如flutter_gallery__transition_perf,然后把它作为run.dart的任务名传入:

../../bin/cache/dart-sdk/bin/dart bin/run.dart -t flutter_gallery__transition_perf

编写一个 DeviceLab 测试(Task)

一个测试就是一个位于bin/tasks下、使用package:flutter_devicelab/framework/framework.dart定义并运行task的简单 Dart 程序。核心 API 是framework.dart中导出的task()函数。

最小示例:

import 'dart:async'; import 'package:flutter_devicelab/framework/framework.dart'; Future<void> main() async { await task(() async { ... do something interesting ... // Aggregate results into a JSONable Map structure. Map<String, dynamic> testResults = ...; // Report success. return new TaskResult.success(testResults); // Or you can also report a failure. return new TaskResult.failure('Something went wrong!'); }); }

要点解读:

  • 一个程序中只允许注册一个task。framework.dart中的_isTaskRegistered标志会在重复注册时抛出StateError('A task is already registered')。不过这一个 task 内部可以运行任意数量的子测试。
  • 每个 task 都有自己的名称,成功与失败相互独立,上报到仪表盘时也相互独立。
  • 每个 task 运行在独立的 Dart VM 中,并通过 Dart VM service 协议(新版本 Dart 下退化为 stdin/stdout 的 IPC 协议,由环境变量FLUTTER_DEVICELAB_IPC控制)上报结果。这样设计可以保证任务之间互不干扰,也让 CI 系统能够对卡住的任务做超时清理。framework.dart注册了三个 VM service 扩展点:ext.cocoonRunnerReady(就绪握手)、ext.cocoonRunTask(触发执行并可携带timeoutInMinutes、localEngine、runProcessCleanup等参数)、ext.cocoonTaskResultReceived(结果确认)。
  • 任务执行完成后,runner 还会执行进程清理:对比任务运行前后机器上的dart进程快照,任何被本测试"泄漏"出的 Dart 进程都会被终止,并导致测试判为失败(相关逻辑在framework.dart的run与 dev/devicelab/lib/framework/running_processes.dart 中)。

关于设备操作系统:任务入口处通常需要声明运行平台。以 dev/devicelab/bin/tasks/complex_layout__start_up.dart 为例,其第一行就是deviceOperatingSystem = DeviceOperatingSystem.android;,随后调用task(createComplexLayoutStartupTest())。DeviceOperatingSystem枚举定义在 dev/devicelab/lib/framework/devices.dart,本仓库的适配版本中除了android、ios、fuchsia、linux、macos、windows等常见平台外,还扩展了ohos(OpenHarmony)值。

将测试接入持续集成(CI)

只有宿主(host-only)类测试应添加到flutter_tools;需要真实设备的任务才属于 DeviceLab 范畴。要把一个新 DeviceLab 任务接入 CI,需要提交若干 PR,其中_TASK_指测试名称,必须与bin/tasks下文件名去掉.dart后缀后的名字一致:

  1. 在根目录的 .ci.yaml 中添加 target;
  2. 镜像一个已有的、使用devicelab/devicelab_dronerecipe 的 target。

如果测试需要在多个操作系统上运行,请为每个操作系统分别创建独立的 target。devicelab_drone是 DeviceLab 在 LUCI 上的标准 recipe,.ci.yaml 中绝大多数物理机测试(如Linux analyzer_benchmark、Linux build_aar_module_test等)都使用该 recipe,并通过task_name属性指向bin/tasks下的任务名。

将测试加入 presubmit

Flutter 的 DeviceLab 在 presubmit(提交前检查)中的容量有限。若希望把一个测试加入 presubmit,需要先提交team-infraissue,由基础设施团队评估可行性后再行添加。

Build + Test 分离模型:更高效地利用设备资源

为了充分利用有限的 DeviceLab testbed 资源并缩短提交验证时间,当前框架支持把构建产物(.apk/.app)与测试执行分离:产物在一台只有宿主(无设备)的 bot(VM 或物理机)上构建,测试则基于该产物在与设备相连的 testbed 上运行。

迁移步骤

  1. 改造任务类:让任务类继承BuildTestTask,并覆写以下四个方法:
    • getBuildArgs:返回传给flutter build的参数;
    • getTestArgs:返回传给flutter drive的参数;
    • parseTaskResult:从测试结果构造TaskResult;
    • getApplicationBinaryPath:返回被测应用产物的路径。
  2. 更新任务入口:将bin/tasks/{TEST}.dart指向新的任务类(例如在其main()中实例化并call()该任务)。
  3. 本地验证:
    • 仅构建:dart bin/test_runner.dart test -t {NAME_OR_PATH_OF_TEST} --task-args build --task-args application-binary-path={PATH_TO_ARTIFACT}
    • 仅测试:dart bin/test_runner.dart test -t {NAME_OR_PATH_OF_TEST} --task-args test --task-args application-binary-path={PATH_TO_ARTIFACT}
  4. 接入 CI:镜像一个 platform 为Linux_build_test或Mac_build_test的 target。与普通 target 的唯一区别是artifact属性:如果省略,将默认使用task_name。
  5. 正式启用:CI 验证通过后,移除 target 上的bringup: true并删除旧的(未采用 build+test 模型的)target 条目,即可在PROD环境启用。

从 dev/devicelab/lib/tasks/build_test_task.dart 的源码可以看到BuildTestTask的完整行为:

  • 构造时解析--build/--test/--application-binary-path三个参数(分别对应常量kBuildOnlyFlag、kTestOnlyFlag、kApplicationBinaryPathOption);
  • build()会先执行flutter clean(除非runFlutterClean为 false),再执行flutter build并把产物拷贝到applicationBinaryPath,供 LUCI recipe 定位并上传产物到 GCS;
  • test()会先解锁设备,再执行flutter drive,最后调用parseTaskResult();
  • call()中若同时传入build与test会抛出异常;仅build时返回TaskResult.buildOnly()("No tests run")。

原仓库在 gallery 相关任务上已经完成了这一迁移(Linux/Android 与 Mac/iOS 各有对应的分离与切换 PR 记录)。

本仓库的 OpenHarmony(ohos)适配扩展

作为 Flutter 的 OpenHarmony 适配版本,本仓库在 DeviceLab 中为 OpenHarmony 平台做了系统性的扩展,可从源码中确认:

  • 任务集:dev/devicelab/bin/tasks 下存在大量以_ohos结尾的任务文件,例如complex_layout__start_up_ohos.dart、flutter_gallery__start_up_ohos.dart、platform_channels_benchmarks_ohos.dart、integration_ui_screenshot_ohos.dart、run_release_test_ohos.dart、basic_material_app_ohos__compile.dart等,覆盖启动性能、滚动流畅度、内存、平台通道、集成 UI、release 运行等场景。
  • 设备抽象:dev/devicelab/lib/framework/devices.dart 中新增了DeviceOperatingSystem.ohos枚举值,并实现了OhosDeviceDiscovery与OhosDevice两个类:设备发现通过hdc list targets完成(自动过滤[Empty]与Offline行);设备控制通过hdc shell系列命令实现,例如用power-shell wakeup/suspend唤醒/休眠、uitest uiInput keyEvent Home模拟 Home 键、aa force-stop停进程、pidof/hidumper --mem读取内存、hilog采集日志、shell reboot重启设备等。
  • 测试覆盖:dev/devicelab/test/hdc_test.dart 等框架测试验证了 hdc 命令的调用行为,例如通过 hdc 发送重启命令等。

这意味着在 OpenHarmony 适配版中,你可以使用与 Android/iOS 完全一致的 DeviceLab 工作流来编写、运行和接入 ohos 设备上的任务:只需要在任务入口设置deviceOperatingSystem = DeviceOperatingSystem.ohos;,并在 CI 的.ci.yaml中镜像devicelab/devicelab_dronerecipe 的 target 即可。

小结

DeviceLab 是 Flutter 真实设备测试的中枢:任务(task)是独立 Dart 程序、运行在独立 VM 中,通过 VM service/IPC 与 runner 通信;本地可通过run.dart/test_runner.dart以-t运行单任务、以--exit关闭自动重试、以--local-engine*系列参数对接本地引擎、以--ab进行引擎性能 A/B 对比;新任务写入bin/tasks后,通过.ci.yaml中devicelab_dronerecipe 接入 CI,并可通过BuildTestTask采用 build+test 分离模型节省设备资源。在本仓库中,这套框架已经完整支持 OpenHarmony 设备(DeviceOperatingSystem.ohos+ hdc 工具链),为在真实 OpenHarmony 硬件上持续验证 Flutter 适配质量提供了坚实底座。

  • 跨平台
  • 前端
  • 移动开发
  • OpenHarmony

【免费下载链接】flutter_flutter

本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。

项目地址:https://gitcode.com/CPF-Flutter/flutter_flutter
点击查看免费下载
上一篇:Neovide 完全指南:用 Rust 打造的 Neovim 图形界面客户端,从安装到配置与源码架构
下一篇:CANN ops-nn ForeachZeroInplace 算子全解析:张量列表原地置零的 aclnn 两段式调用与 NPU 实现原理

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询