You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android截图测试CI与本地模拟器不一致,如何配置相同模拟器?

问题:CI环境截图测试与本地不一致,如何对齐模拟器配置?

我的GitHub Action配置

name: Test and check build

on:
  push:
    branches: ["main", "dev"]
  pull_request:
    branches: ["main", "dev", "hotfix"]

jobs:
  build:
    runs-on: macos-latest
    if: github.event.pull_request.draft == false

    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: "0"
      - name: set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'
          cache: gradle

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
      - name: Run Unit test
        run: ./gradlew testDebugUnitTest
      - name: Run connected tests
        uses: ReactiveCircus/android-emulator-runner@v2
        with:
          api-level: 29
          target: default
          arch: x86_64
          profile: Nexus 6
          script: ./gradlew connectedCheck
      - name: Build
        run: ./gradlew :app:assembleDebug

截图测试代码

/**
 * Simple on-device screenshot comparator that uses golden images present in
 * `androidTest/assets`.
 *
 * Minimum SDK is O. Densities between devices must match.
 *
 * Screenshots are saved on device in `/data/data/{package}/files/${BuildConfig.SCREENSHOTS_FOLDER}.
 */
@RequiresApi(Build.VERSION_CODES.O)
fun assertScreenshotMatchesGolden(
    goldenName: String,
    node: SemanticsNodeInteraction
) {
    val bitmap = node.captureToImage().asAndroidBitmap()

    // Save screenshot to file for debugging
    saveScreenshot(goldenName, bitmap)
    try {
        InstrumentationRegistry.getInstrumentation()
            .context.resources.assets.open("$goldenName.png")
    } catch (ex: Exception) {
        null
    }?.use { BitmapFactory.decodeStream(it) }?.compare(bitmap)
}

private fun saveScreenshot(filename: String, bmp: Bitmap) {
    val canonicalPath =
        InstrumentationRegistry.getInstrumentation().targetContext.filesDir.canonicalPath
    val path = "$canonicalPath/${BuildConfig.SCREENSHOTS_FOLDER}"
    val folder = File(path)
    if (!folder.exists()) {
        folder.mkdirs()
    }
    FileOutputStream("$path/$filename.png").use { out ->
        bmp.compress(Bitmap.CompressFormat.PNG, 100, out)
    }
    println("Saved screenshot to $path/$filename.png")
}

private fun Bitmap.compare(other: Bitmap) {
    if (this.width != other.width || this.height != other.height) {
        throw AssertionError("Size of screenshot does not match golden file (check device density)")
    }
    // Compare row by row to save memory on device
    val row1 = IntArray(width)
    val row2 = IntArray(width)
    for (column in 0 until height) {
        // Read one row per bitmap and compare
        this.getRow(row1, column)
        other.getRow(row2, column)
        if (!row1.contentEquals(row2)) {
            throw AssertionError("Sizes match but bitmap content has differences")
        }
    }
}

private fun Bitmap.getRow(pixels: IntArray, column: Int) {
    this.getPixels(pixels, 0, width, 0, column, width, 1)
}

问题现象

Sizes match but bitmap content has differences

本地使用API 29的Nexus 6模拟器运行截图测试完全正常:先运行一次生成截图并复制到assets文件夹,再次运行测试即可通过。但CI环境执行时出现上述错误,推测是本地和CI的模拟器配置存在差异。

解决方案
  • 锁定系统镜像版本:当前target: default会拉取API 29的最新镜像,本地可能是旧版本,导致渲染差异。先在Android Studio的SDK Manager里查看本地API 29模拟器的镜像ID(比如system-images;android-29;google_apis;x86_64),然后在android-emulator-runner参数里添加system-image: [你的本地镜像ID],强制CI使用和本地完全一致的系统镜像。
  • 统一显示参数:
    • 添加skin: nexus_6,确保模拟器皮肤和本地的Nexus 6完全匹配;
    • 用orientation: portrait或landscape固定屏幕方向,避免本地和CI默认方向不同;
    • 加上density: 480(Nexus 6的标准密度),强制指定屏幕密度,消除密度差异带来的渲染变化。
  • 禁用动画:CI模拟器的动画效果可能和本地不同,导致截图像素差异。在测试脚本前添加三条禁用动画的adb命令,确保界面渲染完全一致:
    adb shell settings put global window_animation_scale 0.0
    adb shell settings put global transition_animation_scale 0.0
    adb shell settings put global animator_duration_scale 0.0
    
  • 验证架构一致性:确认本地模拟器的架构是x86_64,和CI配置里的arch: x86_64保持一致,避免不同架构的渲染差异。

修改后的GitHub Action配置示例

name: Test and check build

on:
  push:
    branches: ["main", "dev"]
  pull_request:
    branches: ["main", "dev", "hotfix"]

jobs:
  build:
    runs-on: macos-latest
    if: github.event.pull_request.draft == false

    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: "0"
      - name: set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'
          cache: gradle

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
      - name: Run Unit test
        run: ./gradlew testDebugUnitTest
      - name: Run connected tests
        uses: ReactiveCircus/android-emulator-runner@v2
        with:
          api-level: 29
          target: google_apis # 和本地镜像的target一致,比如default或google_apis
          arch: x86_64
          profile: Nexus 6
          skin: nexus_6
          density: 480
          orientation: portrait
          system-image: system-images;android-29;google_apis;x86_64 # 替换成本地实际的镜像ID
          script: |
            adb shell settings put global window_animation_scale 0.0
            adb shell settings put global transition_animation_scale 0.0
            adb shell settings put global animator_duration_scale 0.0
            ./gradlew connectedCheck
      - name: Build
        run: ./gradlew :app:assembleDebug

内容的提问来源于stack exchange,提问作者1048576

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 00:00:10