- 跨平台
- 前端
- 移动开发
- OpenHarmony
【免费下载链接】flutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
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中,规则如下:
- 任务成功:runner 上报成功,并将该任务记录的性能指标上传到 Flutter 基础设施。注意,并非所有任务都会记录性能指标——只有通过
benchmarkScoreKeys声明了评分键的任务才会产生可上报的指标(见 dev/devicelab/lib/framework/task_result.dart 中TaskResult.success对benchmarkScoreKeys的校验逻辑)。 - 任务失败后自动重跑:每当最后一次运行成功,任务最终仍会被记为成功,但同时会标记一个flake(不稳定)标志并写入测试结果。源码中重试次数由 dev/devicelab/lib/framework/metrics_result_writer.dart 的
MetricsResultWriter.retryNumber = 2决定,即默认最多自动重试 2 次;runner 在failureCount > 0但最终成功时会打印flaky: true。 - 所有重试均失败: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后缀后的名字一致:
- 在根目录的 .ci.yaml 中添加 target;
- 镜像一个已有的、使用
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 上运行。
迁移步骤
- 改造任务类:让任务类继承
BuildTestTask,并覆写以下四个方法:getBuildArgs:返回传给flutter build的参数;getTestArgs:返回传给flutter drive的参数;parseTaskResult:从测试结果构造TaskResult;getApplicationBinaryPath:返回被测应用产物的路径。
- 更新任务入口:将
bin/tasks/{TEST}.dart指向新的任务类(例如在其main()中实例化并call()该任务)。 - 本地验证:
- 仅构建:
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}
- 仅构建:
- 接入 CI:镜像一个 platform 为
Linux_build_test或Mac_build_test的 target。与普通 target 的唯一区别是artifact属性:如果省略,将默认使用task_name。 - 正式启用: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。
相关推荐
3 步隐藏 macOS 菜单栏图标:Ice 菜单栏管理上手指南
3 步隐藏 macOS 菜单栏图标:Ice 菜单栏管理上手指南 MacBook 用了两年,菜单栏右半边已经挤了 20 多个图标,常用的三个还总被挤到刘海下面。
桌面应用在 PR 上重跑 Flutter DeviceLab 测试:基于 LUCI `led` 的完整实战指南
在 PR 上重跑 Flutter DeviceLab 测试:基于 LUCI led 的完整实战指南 DeviceLab(设备实验室)是 Flutter 在真实物
跨平台移动开发前端UI组件桌面应用Flutter 团队 espresso 包实战指南:在 Android 上为 Flutter 应用编写与运行 Espresso 测试
Flutter 团队 espresso 包实战指南:在 Android 上为 Flutter 应用编写与运行 Espresso 测试 导读 espresso 是
移动开发跨平台
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考