首页
/ Flutter Android Hardware Smoke Test:面向 OEM 的自包含硬件渲染一致性验证套件

Flutter Android Hardware Smoke Test:面向 OEM 的自包含硬件渲染一致性验证套件

2026-09-04 20:11:44作者:昌雅子Ethen

本篇围绕 Flutter 仓库中的 android_hardware_smoke_test 集成测试套件展开,系统讲解它如何以**“宿主驱动 + 设备端插桩”**双模式架构,在不依赖 Flutter SDK、Dart CLI 或宿主编排的前提下,直接在 Android 真机/模拟器上完成视觉渲染、GPU 驱动与硬件兼容性的像素级回归验证。读完本文,你将能够完整掌握该套件的运行方式、双模式协作链路、Platform View 截图策略,以及针对 EGL 初始化失败与空白截图的两层重试稳定性机制,并可据此在自己的硬件目标设备上复现这套验证流程。

1. 前置准备与初始构建

该集成测试项目遵循最小样板(minimal-boilerplate)模式,标准的二进制 Gradle Wrapper 与属性文件并不提交到仓库。因此在第一次本地编译或运行任何连机 JUnit/插桩测试之前,必须先在该目录下执行标准的 Flutter 项目再生成命令,以恢复缺失的 Wrapper:

flutter create --platform=android --no-overwrite .

这条命令会干净地恢复缺失的 Wrapper 脚本(gradlewgradlew.bat)与 Wrapper 配置,而不会改动任何定制化的构建定义、Java/Kotlin 测试骨架或包源码。CI 侧的运行器 run_android_hardware_smoke_tests.dart 中的 _regenerateAndroidWrappers 函数正是执行了同样的 flutter create --platform=android --no-overwrite .,印证了这一前置步骤在 CI 流水线中同样是强制执行的第一环。

2. 概述与目标:完全自包含的插桩测试套件

android_hardware_smoke_test 的核心目标是提供一个完全自包含(fully self-contained)的 Android 插桩测试套件

这使得 Android 硬件厂商(OEM)能够在一个预编译好的 APK 上,直接针对其目标设备运行视觉回归、性能与 GPU 兼容性测试。测试执行不需要 Flutter SDK、Dart CLI 命令或任何宿主侧编排。 结果反馈直接体现在标准的原生 Android JUnit 测试报告中。

从源码结构看,这一“自包含”特性体现在 FlutterActivityTest.kt 中:测试通过 ActivityScenarioRule(MainActivity::class.java) 直接拉起被测 Activity,并通过 BasicMessageChannel 与 Dart 侧通信,整个流程由原生 JUnit 完成,无需任何 Dart 驱动参与。

与 android_engine_test 的结构差异

两套套件都验证渲染正确性,但为支持不同工作流而在架构上做了不同设计:

维度 android_engine_test android_hardware_smoke_test
验证位置 仅宿主(CI PC) 双模式:设备端(OEM)+ 宿主端(CI)
黄金比对 仅宿主侧,对比 Skia Gold。 OEM 模式:应用内像素比对,对比打包资产。
CI 模式:宿主侧对比仓库文件。
设备角色 被动目标,仅渲染静态视图。 主动参与,执行设备端 JUnit 编排。
目标受众 核心引擎贡献者与 CI 分片。 Android 硬件厂商(OEM)与 CI 分片。

3. 双模式架构

下图给出该套件的整体执行流(继承自原文档架构图):

flowchart TD
    TestInit(["Test Initiation"]) --> Mode{"Which Mode?"}

    Mode -->|"Host-Driven Mode"| HostScript["driver script<br/>(test_driver/driver_test.dart)"]
    Mode -->|"Instrumented Mode"| JUnit["native Android JUnit<br/>(FlutterActivityTest)"]

    HostScript --> RequestData["Driver script connects and requests<br/>testName via driver.requestData()"]
    JUnit --> SendMessage["JUnit test runner sends<br/>testName over Message Channel"]

    RequestData --> RenderHost["Dart app renders target state"]
    SendMessage --> RenderDevice["Dart app renders target state"]

    RenderHost --> HostRender["Dart app returns image bytes over channel"]
    RenderDevice --> OnDeviceCompare["Dart app performs local on-device golden<br/>comparison against bundled assets"]

    HostRender --> CompareHost["Driver script asserts exact match<br/>against local filesystem"]
    OnDeviceCompare --> Report["Dart app returns success/fail<br/>to JUnit test runner"]

宿主驱动模式(CI / Host-Driven)

  • 编排:由宿主机通过 flutter drive 驱动。

  • 执行:宿主脚本 driver_test.dart 通过 integration_test/integration_test_wrapper.dart 这个薄包装层向应用(main.dart)下发状态切换指令。应用截取重绘边界(RepaintBoundary),将其 base64 编码,再通过消息通道把字节流回宿主。宿主驱动解码字节,并在宿主文件系统上对照本地仓库基线断言视觉一致。

    在源码层面,宿主端通过 flutterDriver.requestData(...) 发送 testName 并设置 keyPerformAppSideGoldenCompare: false,Dart 端 _handleStandardViewRequest 捕获 PNG 字节后以 keyImageBytes: base64.encode(...) 回传,宿主再调用 matchesGoldenFile('goldens/$testName$activeGoldenVariant.png') 完成比对。此外,LUCI CI 环境下会自动启用 Skia Gold 比对器(isLuci 判定 + enableSkiaGoldComparator)。

插桩设备端模式(OEM / Standalone)

  • 编排:完全运行在设备上,使用 Android AndroidJUnit4 Runner。
  • 执行:Kotlin JUnit 代码(FlutterActivityTest.kt)拉起主 Activity,通过 JSON 消息通道下发测试负载。应用(main.dart)渲染 widget,对打包在 APK 资产内的基线图像执行本地逐像素的设备端比对,再把状态回传给 Java Runner,从而让 JUnit 断言通过或失败。

两种模式共享同一套 Dart 侧处理逻辑(goldens.darthandleGoldenRequest),仅通过 performAppSideGoldenCompare 布尔开关决定是在设备上比对还是回传字节给宿主。

图形后端的静态单一数据源

静态编译的单一数据源(Single Source of Truth)

应用编译后的 AndroidManifest.xml 中的 <meta-data> 标签,是两种模式下图形后端配置的唯一权威来源

  • 插桩设备端模式(OEM):原生 Kotlin JUnit 骨架(FlutterActivityTest.kt)通过 PackageManager API 动态读取该值并路由给 Dart。
  • 宿主驱动模式(CI / Host):Dart 应用通过自定义 MethodChannel 向原生 Android embedder 查询,自行发现其编译时后端,并在 JSON 应答负载中自报告给宿主测试脚本,从而完全消除宿主 PC 上对环境变量的依赖

这一机制在源码中得到印证:

  • MainActivity.ktprovideFlutterEngine 中,通过 packageManager.getApplicationInfo(..., GET_META_DATA) 读取 io.flutter.embedding.android.ImpellerBackend,并在 MethodChannelimpeller_backend 方法上返回该值。
  • main.dartinitState 中调用 _nativeChannel.invokeMethod<String>(methodImpellerBackend) 获取该值,作为 goldenVariant 使用,用于选择对应的黄金基线。

当前仓库中 AndroidManifest.xml 的默认配置如下:

<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="true" />
<meta-data android:name="io.flutter.embedding.android.ImpellerBackend" android:value="vulkan" />
<meta-data android:name="io.flutter.embedding.android.EnableHcpp" android:value="true" />
<meta-data android:name="flutterEmbedding" android:value="2" />

要手动切换本地运行时的图形后端,打开 android/app/src/main/AndroidManifest.xml 并更新 io.flutter.embedding.android.ImpellerBackend 的值:

<!-- 启用 Vulkan: -->
<meta-data android:name="io.flutter.embedding.android.ImpellerBackend" android:value="vulkan" />

<!-- 启用 OpenGLES: -->
<meta-data android:name="io.flutter.embedding.android.ImpellerBackend" android:value="opengles" />

CI 运行器 run_android_hardware_smoke_tests.dart 中的 _setAndroidManifestBackend 正是用正则替换这一 meta-data 标签,并在 finally 块中恢复原始内容,确保 Git worktree 干净。

4. 如何运行测试

执行任何命令前,请先解锁已连接的 Android 设备或模拟器,并确保其处于活跃状态。

A. 通过宿主驱动运行(CI / Host-Driven)

该模式用于在本地 PC 或 CI 流水线上执行视觉断言,并管理本地黄金基线。

运行驱动测试套件的命令

# 从 android_hardware_smoke_test 根目录执行
flutter drive -v \
  --driver=test_driver/driver_test.dart \
  --target=integration_test/integration_test_wrapper.dart \
  --no-dds

捕获/更新参考黄金基线的命令

UPDATE_GOLDENS=true 运行时,会向宿主上的 test_driver/goldens/ 写入或覆盖本地 PNG 基线。

由于静态编译的 AndroidManifest.xml 是唯一权威来源,应用会自动自报告其当前后端变体。要捕获或更新特定图形变体的基线,只需编辑 android/app/src/main/AndroidManifest.xml 设置期望的 io.flutter.embedding.android.ImpellerBackend 值,然后执行:

UPDATE_GOLDENS=true flutter drive -v \
  --driver=test_driver/driver_test.dart \
  --target=integration_test/integration_test_wrapper.dart \
  --no-dds

说明:仓库内 test_driver/goldens/README.md 进一步明确了“本地 vs CI”的黄金管理模型——本地运行与独立 OEM 模式在宿主 PC 上以 UPDATE_GOLDENS 捕获基线,这些 PNG 会在独立设备端执行时作为只读资产编译进插桩测试 APK;而 CI 运行时不应把生成的 PNG 提交到仓库,宿主侧比对会自动路由到中央 Skia Gold 后端。该 README.md 的存在也确保了 Git 保留该目录,避免 CI 预检时的 Flutter 资产打包告警。

B. 运行插桩测试(OEM / Self-Contained)

资产打包前置条件 由于插桩测试完全独立运行在设备上,它们会把像素与打包在 APK 内的只读资产基线图像做比对。在编译构建插桩 APK 之前,必须先使用宿主驱动模式(UPDATE_GOLDENS=true)在 test_driver/goldens/ 下生成本地基线。

编译并运行原生 JUnit 套件的命令

# 从 'android' 子目录执行
cd android
./gradlew :app:connectedDebugAndroidTest \
  -Pandroid.testInstrumentationRunnerArguments.class=com.example.android_hardware_smoke_test.FlutterActivityTest \
  -s

自动化 HTML 截图嵌入(embedTestResultImages

运行上述 Gradle 命令时,一个名为 embedTestResultImages 的自定义 Kotlin DSL 任务会在测试结束后自动执行。

它会无缝完成以下动作:

  1. 通过 gradle.properties 注入,阻止 Gradle 过早自动卸载 APK;
  2. 查询设备沙盒缓存,动态发现所有已渲染的 .png 文件;
  3. 使用零拷贝 ADB 管道把原始二进制图像流式传输到宿主 PC;
  4. 动态解析生成的 HTML 报告,在测试结果表格单元格旁注入替代 <img> 元素;
  5. 执行手动 adb uninstall 清理,让目标设备保持完全干净。

完成后,打开 app/build/reports/androidTests/connected/debug/index.html,即可看到原生内嵌了所有渲染结果快照的交互式报告。

从源码看,test_result_embedder.gradle.kts 通过从 AGP BaseExtension 解析二进制安全的 adb 可执行文件,再用 ProcessBuilder 执行 adb shell run-as <package> ls cache/resultsadb exec-out run-as <package> cat cache/results/<file> 拉取图像(因结果写在设备临时目录,adb pull 无权限,必须借助 run-as + cat),最后遍历 HTML 报告把 <img> 注入到对应测试单元格旁。

C. 手动命令行调试(绕过 Gradle 编排)

如果想完全绕过 Gradle 进行自定义调试,可以用裸 adb shell 调用手动构建、安装并运行插桩测试:

  1. 手动构建并安装包

    # 从 'android' 子目录执行
    cd android
    ./gradlew installDebug installDebugAndroidTest
    
  2. 手动启动原生 Android 插桩测试

    adb shell am instrument -w \
      -e class com.example.android_hardware_smoke_test.FlutterActivityTest \
      com.example.android_hardware_smoke_test.test/androidx.test.runner.AndroidJUnitRunner
    
  3. 手动从设备沙盒拉取生成的快照: 由于在裸 adb 运行时应用保持安装状态,可以手动复制渲染结果文件:

    adb exec-out "run-as com.example.android_hardware_smoke_test cat cache/results/blueRectangleTest.png" \
      > test_driver/results/blueRectangleTest.png
    

D. 本地运行 CI 分片

由于该测试套件注册在中央仓库测试编排器 dev/bots/test.dart 中,你可以使用标准 dev-bot 脚本在本地执行完整 CI 运行器流水线:

# 本地运行 Vulkan 图形后端分片
SHARD=android_hardware_smoke_vulkan_tests bin/cache/dart-sdk/bin/dart dev/bots/test.dart

# 本地运行 OpenGLES 图形后端分片
SHARD=android_hardware_smoke_opengles_tests bin/cache/dart-sdk/bin/dart dev/bots/test.dart

5. 测试套件覆盖范围与技术理由

该套件由若干针对性的视觉回归测试用例组成,用于驱动不同的 GPU 图形管线,并验证其与驱动和硬件配置的兼容性:

测试用例 渲染管线 验证的 Impeller/硬件机制 参考/来源
blueRectangleTest 纯色矢量填充 标准矢量光栅化与布局变换。 简单 canvas.drawRect
trianglePathTest 复杂路径 路径三角化、光栅化与硬件抗锯齿(MSAA)。 简单 canvas.drawPath
textTest 字体渲染 文本布局(TextPainter)、字形缓存、 shaping 与字体图集渲染。 简单 TextPainter.paint
imageTest 纹理采样 图像解码、GPU 纹理上传与纹理采样器渲染。使用 32x32 四色棋盘 PNG 验证 RGB 色彩通道正确性。 简单 canvas.drawImage
advancedBlendTest 高级混合 片段着色器混合与帧缓冲取回(framebuffer fetch)的 tile-memory 优化(如 Vulkan subpass 输入、GLES 的 EXT_shader_framebuffer_fetch)。使用 BlendMode.difference 镜像 animated_advanced_blend.dart
backdropFilterBlurTest 合成与模糊 离屏纹理分配、图层下采样/上采样 pass,以及多 pass 高斯模糊滤镜执行。使用 ImageFilter.blur(sigmaX: 5, sigmaY: 5) 镜像 backdrop_filter.dart
platformViewTextureLayerTest 平台视图(Texture Layer) 通过 PlatformViewsService.initSurfaceAndroidView 使用 Texture Layer Hybrid Composition(TLHC)嵌入原生 Android 视图合成。 镜像纹理层合成路径。
platformViewHybridCompositionTest 平台视图(Hybrid Composition) 通过 PlatformViewsService.initExpensiveAndroidView 使用传统 Hybrid Composition(HC)嵌入原生 Android 视图合成。使用 AndroidViewSurface 覆盖原生 UI。 镜像传统混合合成路径。
platformViewHybridCompositionPlusPlusTest 平台视图(Hybrid Composition++) 通过 PlatformViewsService.initHybridAndroidView 使用 Hybrid Composition++(HCPP)嵌入原生 Android 视图合成。若设备不支持 HCPP 硬件则干净跳过。 镜像现代 HCPP 合成路径。

从源码看,imageTest 的图像是懒加载的:main.dart_loadImage 仅在 testName == kImageTest 且尚未加载时触发,并显式捕获加载失败以让宿主驱动测试明确失败。HCPP 用例(kPlatformViewHybridCompositionPlusPlusTest)在 HybridAndroidViewController.checkIfSupported() 返回不支持时,会以 Skipped 状态加原因(需要 Vulkan 与 Android 14+)返回,宿主与设备端均会据此标记测试为跳过。

6. Platform View 截图策略

对平台视图进行测试,需要截取一张同时包含 Flutter 渲染 UI 与原生 Android 视图(例如混合合成下包装的原生 TextView)的物理屏幕布局截图。

由于这两个上下文渲染在分离的硬件表面层上,标准的进程内 widget 截图方法(如 RenderRepaintBoundary.toImage()无法看到或捕获原生平台视图的像素。为此,该测试套件实现了一种**“无合成系统截图策略(No-Compositing System Screenshot Strategy)”**:

flowchart TD
    TestInit(["Test Initiation"]) --> Mode{"Which Mode?"}

    Mode -->|"Host-Driven Mode"| HostScript["driver script<br/>(test_driver/driver_test.dart)"]
    Mode -->|"Instrumented Mode"| JUnit["native Android JUnit<br/>(FlutterActivityTest)"]

    HostScript --> RequestData["Driver script connects and requests<br/>testName via driver.requestData()"]
    JUnit --> SendMessage["JUnit test runner sends<br/>testName over Message Channel"]

    RequestData --> RenderHost["Dart app renders target state"]
    SendMessage --> RenderDevice["Dart app renders target state"]

    RenderHost -->|"Unique for PlatformView tests"| RenderHostPlatformView["Native Kotlin UI renders via PlatformView"]:::unique
    RenderDevice -->|"Unique for PlatformView tests"| RenderDevicePlatformView["Native Kotlin UI renders via PlatformView"]:::unique

    RenderHostPlatformView --> DrawSyncHost["Native view triggers onDraw;<br/>Dart awaits callback + 1 frame"]:::unique
    DrawSyncHost --> CoordsHost["Dart app returns widget<br/>crop coordinates"]:::unique

    RenderDevicePlatformView --> DrawSyncDevice["Native view triggers onDraw;<br/>Dart awaits callback + 1 frame"]:::unique
    DrawSyncDevice --> CoordsDevice["Dart app returns widget<br/>crop coordinates"]:::unique

    CoordsHost --> CaptureHost["Driver script takes ADB screencap<br/>and crops locally"]:::unique
    CoordsDevice --> CaptureDevice["JUnit test runner takes UiAutomation<br/>screencap and crops natively"]:::unique

    CaptureHost -->|"Remaining steps occur normally"| CompareHost["Driver script asserts exact match<br/>against local filesystem"]
    CaptureDevice --> SendBytes["JUnit test runner sends cropped<br/>base64 bytes to Dart app over message channel"]:::unique

    SendBytes --> ImmediateComparison["App handles cropped bytes immediately without new frame render"]:::unique

    ImmediateComparison -->|"Remaining steps occur normally"| OnDeviceCompare["Dart app performs local on-device golden<br/>comparison against bundled assets"]

    OnDeviceCompare --> Report["Dart app returns success/fail<br/>to JUnit test runner"]

    classDef unique fill:#fff3cd,stroke:#ffc107,color:#856404,stroke-width:2px;

流程拆解

测试的开头与结尾和上文第 3 节描述的其它测试相同。

  • 原生 UI 渲染与时序: 对平台视图测试(以 platformView 前缀开头的用例),应用会渲染 AndroidViewLinkAndroidView widget。这会触发 Android 系统通过选定的合成模式(见 NativeTextView.kt)渲染原生 Kotlin UI(NativeTextView)。由于原生 UI 由 OS 渲染,addPostFrameCallback 已不足以保证所有平台视图像素都已完全渲染并合成。为解决此问题,原生 ObservableTextView 重写了 onDraw(Canvas),在原生视图真正绘制时通过方法通道回调 "onDraw"。Dart 应用通过 goldens.dart 中的 settleFuture 等待该回调,然后使用 await WidgetsBinding.instance.endOfFrame 再等待 1 帧,以确保平台视图合成已完全提交。

    在源码中,MainActivity.kt 注册原生视图工厂 com.example.android_hardware_smoke_test/native_text_view,其绘制回调即调用 methodChannel?.invokeMethod("onDraw", null);而 main.dart_nativeChannel 上监听 onDraw 方法并补齐 _platformViewDrawnCompleter,该 completer 的 future 即作为 settleFuture 传入 handleGoldenRequest

  • 裁剪坐标与测试脚本驱动的截图捕获

    • 两种模式:Dart 应用(goldens.dart 中的 _handlePlatformViewRequest)计算 RepaintBoundary 在物理设备像素下的包围盒,并把这些坐标返回给测试运行器。源码中通过 renderObject.localToGlobal(Offset.zero) 得到逻辑坐标,再乘以 devicePixelRatio 得到物理像素的 x/y/width/height 回传。
    • 宿主驱动模式:驱动脚本使用 ADB 对物理设备做全屏截图(封装在 driver_test.dartNativeDriver.screenshot() 内的 adb shell screencap),然后用 Dart image 包按取回的坐标在本地裁剪。
    • 插桩模式:JUnit 测试运行器通过 FlutterActivityTest.ktcaptureAndSendScreenshot 使用 UiAutomation.takeScreenshot() 做全屏截图,再用 Bitmap.createBitmap 原生裁剪位图。
  • 插桩模式的额外往返与编码无关的像素比对

    • 插桩模式:Dart 应用是执行黄金比对的地方,因此 JUnit 测试运行器会把裁剪后的字节 base64 编码,通过 BasicMessageChannel 发回 Dart 应用,并把 JSON 消息中的 command 字段设为 compare_golden。当 Dart 应用解析到该请求时,它会立即执行黄金比对,而不是再通过 addPostFrameCallback 等待另一帧。然而,常规的 NaiveLocalFileComparator 会比对所有图像字节,这存在问题:不同方式捕获的截图可能产生 PNG 编码差异。为无论编码如何都能比对像素,代码改用 PixelExactLocalFileComparator。这个自定义比较器还支持通过 asset:// URI 方案,利用 rootBundle.load() 直接从包打包资产中读取黄金参考,完全避免了把文件复制到设备临时目录的需要。比对完成后,它与其它测试一样报告成功或失败。

    从源码看,pixel_exact_local_file_comparator.dartcompare 方法会把两张图各自解码为原始像素(ui.instantiateImageCodecgetNextFrame),再逐字节比较 toByteData() 结果。类注释明确解释了原因:Android 原生 screencap 压缩器(libpng)与 Dart 的 image 编码器对相同像素网格会产生不同的元数据、chunk 顺序与 zlib 压缩级别,因此在设备上逐字节比对压缩 PNG 是不稳定的。

    • 宿主驱动模式:驱动不需要新的往返,因为它对从 Dart 应用返回的图像字节直接以相同方式执行比对。

为何不采用 PixelCopy 方案

一个使用 PixelCopy 的替代方案曾被考虑:

  • 其工作方式:原生 Kotlin 应用直接对 FlutterSurfaceView 调用 PixelCopy.request(surfaceView, ...),然后手动遍历 Android 兄弟视图层级,使用 Kotlin Canvas 把可见的平台视图边界绘制在捕获的位图之上。

虽然该方案可行且能工作,但因以下几个原因并非理想选择

  1. 手动合成复制:手动把视图绘制到画布(child.draw(canvas))依赖对合成步骤的复现。如果操作系统或图形驱动在硬件合成期间施加了特定着色器效果、混合、自定义覆盖层或亚像素抗锯齿,手动 Kotlin 重构可能与用户实际看到的画面不一致。
  2. 遗漏真实合成 Bug:该 smoke 测试的首要目标是捕捉 GPU 驱动中与平台相关的集成和合成渲染错误。如果手动绘制兄弟视图,截图就完全绕过了 OS 硬件合成器(SurfaceFlinger),从而违背了测试系统真实合成管线的目的。
  3. 脆弱性:在 Kotlin 中手动转换坐标空间、处理布局、视图可见性与 Z-order 高度脆弱,容易出现模拟/渲染 Bug。

通过用原生系统合成器 API(adb / UiAutomation)截取整屏并裁剪,测试断言的是设备 GPU 与系统合成器渲染出的真实、最终合成图像

为何不采用“编译在线 / 运行离线”拆分

另一个被考虑的执行模型是:

  • 其工作方式:构建拆成两个顺序阶段——在线编译测试 APK(assembleDebugAndroidTest),随后完全离线运行测试执行(connectedDebugAndroidTest --offline),以隔离模拟器执行与 Maven 网络抖动。

虽然对网络隔离有益,但因以下原因并非理想选择

  1. UTP 动态解析设计:Android Gradle Plugin(AGP)8.x/9.x 的统一测试平台(UTP)Runner 在运行时通过内部 detached 配置动态解析并下载其宿主侧 Runner 插件(如 com.android.tools.utp:android-test-plugin-host-additional-test-output)。这些依赖在编译期间不会被解析,导致冷缓存下离线检查失败。
  2. 维护开销 / 双重执行:为支持离线运行,要么先执行一次带 try-catch 的在线测试(导致整个测试套件执行两次,浪费 CI 资源),要么手动在 build.gradle.kts 中锁定并声明所有传递性 UTP 依赖。硬编码 UTP 插件版本非常脆弱,项目升级 AGP 版本时会自动失效。
  3. 冗余的安全考量:由于在线编译阶段(assembleDebugAndroidTest)已经会向远程 Maven 仓库查询项目依赖,在线运行测试执行并不会引入任何新的可能抖动的网络向量。Gradle 内部依赖缓存保证后续执行极其快速。

因此直接在线执行 connectedDebugAndroidTest(单步完成),测试套件恰好执行一次、在 AGP 升级中维护成本低,且能干净地上抛任何真实的编译器或运行器错误,而无需静默的 try-catch 块。

7. 图形初始化与截图重试机制

为保证在模拟器与物理硬件上对抗抖动的高稳定性,该测试套件实现了两层重试机制:

A. 进程与 Activity 级重试(图形上下文初始化)

  • 目标:恢复 Flutter Engine 启动时全局发生的瞬态 EGL 或图形上下文协商错误(例如 Failed to choose config with EGL_SWAP_BEHAVIOR_PRESERVED 或 HWUI 启动警告)。

  • 机制

    • 宿主端(CI):若 flutter drive 运行失败,宿主套件运行器 run_android_hardware_smoke_tests.dart 会检查设备 logcat。若检测到瞬态 EGL 错误,它会重启测试运行进程(最多 3 次尝试),以在全新进程中初始化新的 EGL 上下文。源码中 _runRetryLoop 在每次尝试前执行 adb logcat -c 清空缓冲,再根据检测结果决定重试。
    • 设备端(JUnit):若一次尝试失败,JUnit 运行器 FlutterActivityTest.kt 会重建 Activity 场景并仅对空白截图失败进行重试(最多 3 次)。它不会在 EGL/图形错误上重试,因为在同一应用进程内重建 Activity 无法恢复进程级 EGL 初始化失败;这类失败会在设备上立即失败,以冒泡到宿主运行器。标准测试失败(如黄金像素不匹配)也会立即失败。

    源码中这一策略体现得十分清晰:templateTest 的循环里,只有 isBlankScreenshotException(e) 为真才会 rule.scenario.recreate() 重试;否则抛出不可重试的 RuntimeException。而 verifyNoGraphicsPipelineErrors 通过 logcat -d --pid=$pid *:W 抓取本进程日志,一旦命中 libEGL/HWUI/EGL_ 关键字即抛 EglInitializationException,并调用 MainActivity.evictEngineCache() 清掉缓存引擎,确保后续尝试从干净的引擎实例开始。

B. 截图级重试(空白平台视图捕获)

  • 目标:防止截图在原生平台视图合成完全稳定之前被捕获、从而产生空白/透明帧的竞态条件。
  • 机制
    • 两种模式:截图捕获循环(原生 UiAutomation 与宿主侧 NativeDriver)都会检查裁剪后的图像。若裁剪后的图像完全透明或纯黑,则以 200ms 间隔重试捕获,最多 3 次。源码中 FlutterActivityTest.ktisBitmapBlank 通过 getPixels 判断是否所有像素都等于首像素且首像素为透明/纯黑;driver_test.dart 中对应使用 isImageBlank
    • 短路(Short-circuiting):若截图尝试期间在 logcat 中检测到 EGL/图形错误,循环会提前终止(跳过剩余截图重试),以尽快让测试失败并把失败尽快冒泡到宿主运行器。设备端对应 hasGraphicsPipelineErrors(marker) 检测到错误后立即 break;而 MainActivity.kt 还在 onCreate 中通过 window.setFormat(PixelFormat.RGBA_8888) 把窗口像素格式锁定为 8 位 sRGB,以防止 SwiftShader 驱动上的 HWUI 10 位格式协商错误,进一步降低设备端的图形初始化抖动。

小结

android_hardware_smoke_test 通过一套“单一数据源(Manifest meta-data)+ 双模式编排 + 无合成系统截图 + 两层重试”的设计,把一个原本依赖宿主 SDK 的视觉回归验证,改造成了可在 OEM 目标设备上独立运行的原生 JUnit 套件。对贡献者而言,它既能在 CI 中以宿主驱动模式接入 Skia Gold 与本地分片,又能在设备端以插桩模式自校验打包黄金基线;对硬件厂商而言,它则提供了一个无需 Flutter SDK、结果直接落入原生 JUnit 报告即可交付的渲染兼容性验证工具。理解 main.dart 的消息处理、goldens.dart 的坐标/截图分流、FlutterActivityTest.kt 的重试与日志自诊断,以及 PixelExactLocalFileComparator 的编码无关像素比对,是读懂并复现整套机制的关键。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341