基于 ADB + FFmpeg 的 Android 设备集群:实现 10–50 台设备的自动化 UI 测试

@ridark_eth
英语2026年7月07日
261K
111
12
23
200

TL;DR

本指南提供了一套 Python 流水线,利用 ADB 和 FFmpeg 在数十台 Android 设备上实现 APK 安装、UI 测试及视频报告生成的自动化,并包含详细的成本效益分析。

当你的应用需要在几十台真实手机、不同厂商、不同 Android 版本和屏幕分辨率的"动物园"中运行时,手动测试很快就会变成一场噩梦。下面是一个 Python 管道,它能自动发现所有连接的设备,在所有设备上并行安装 APK,运行插桩测试,录制每台设备的运行视频,并用 FFmpeg 将这些录像拼接成一个视频报告。

整个技术栈 > Python + ADB + FFmpeg < 是标准的 QA 工具。没有魔法,只是把繁琐的工作自动化了。

Ridark - inline image

管道架构

text
1adb devices ──► 设备序列号列表
2
3
4APK 安装 (在所有设备上并行,使用 ThreadPoolExecutor)
5
6
7对每台设备:
8 屏幕录制 (后台运行) → am instrument (运行测试) → 停止并拉取视频
9
10
11FFmpeg:在每个片段上叠加序列号 + 拼接 ──► test_report.mp4

你需要准备什么

  • ADB(Android 调试桥)来自 Android 平台工具 -> 设备控制。
  • Python 3.10+ -> 编排(我使用 list[str]、tuple[...],无需 from __future__)。
  • FFmpeg -> 视频处理和组装。
  • 已启用 USB 调试 的设备,通过 USB 连接(或通过 adb tcpip 使用 Wi-Fi)。

贯穿所有代码的一个原则:我将参数以 列表 形式传递给子进程,并且 不使用 shell=True。这样更安全(不会通过文件名注入),也不会因路径中的空格或特殊字符而出错。

1. 设备发现

Ridark - inline image

adb devices 也会列出未授权/离线状态的设备。我们只保留实际处于"device"状态的设备。

python
1import subprocess
2
3def get_devices() -> list[str]:
4 """返回所有处于 'device' 状态的设备序列号。"""
5 out = subprocess.run(
6 ["adb", "devices"],
7 capture_output=True, text=True, check=True,
8 ).stdout
9
10 serials: list[str] = []
11 for line in out.splitlines()[1:]: # 第一行是 "List of devices" 标题
12 line = line.strip()
13 if line.endswith("\tdevice"): # 排除 unauthorized / offline
14 serials.append(line.split("\t")[0])
15 return serials

2. 并行 APK 安装

在 50 台设备上一台接一台地安装很慢。我们将任务分散到线程池中:每个 adb install 都是一个独立的进程,因此线程在这里效果很好(我们等待的是 I/O,而不是消耗 CPU)。

python
1from concurrent.futures import ThreadPoolExecutor, as_completed
2
3def install_apk(serial: str, apk_path: str) -> tuple[str, bool, str]:
4 r = subprocess.run(
5 ["adb", "-s", serial, "install", "-r", "-g", apk_path],
6 capture_output=True, text=True,
7 )
8 ok = r.returncode == 0 and "Success" in r.stdout
9 return serial, ok, (r.stdout + r.stderr).strip()
10
11def install_on_all(apk_path: str, serials: list[str]) -> None:
12 with ThreadPoolExecutor(max_workers=len(serials) or 1) as pool:
13 futures = [pool.submit(install_apk, s, apk_path) for s in serials]
14 for f in as_completed(futures):
15 serial, ok, log = f.result()
16 print(f"[{'OK' if ok else 'FAIL'}] {serial}")
17 if not ok:
18 print(f" {log}")

标志说明:-r -> 重新安装并保留数据,-g -> 立即授予所有运行时权限(方便测试不会因权限对话框而中断)。

3. 运行插桩测试

am instrument 在设备上运行 Espresso/JUnit 测试。成功时输出 OK,失败时输出 FAILURES!!! 到 stdout -> 我们据此判断结果。

python
1def run_instrumented_tests(
2 serial: str,
3 package: str,
4 runner: str = "androidx.test.runner.AndroidJUnitRunner",
5) -> tuple[str, bool]:
6 r = subprocess.run(
7 ["adb", "-s", serial, "shell", "am", "instrument", "-w",
8 f"{package}/{runner}"],
9 capture_output=True, text=True,
10 )
11 ok = "FAILURES!!!" not in r.stdout and r.returncode == 0
12 return serial, ok

package 是测试包 ID,通常是 com.example.app.test。

4. 在测试期间录制屏幕

screenrecord 直接在设备上录制视频。需要注意的限制:每个文件约 3 分钟的上限,并且 没有音频。我们在后台开始录制,运行测试,然后干净地停止并拉取文件到主机。

停止录制最可靠的方法不是通过本地 adb 发送信号,而是在设备本身上使用 pkill -> 这样 screenrecord 能正确完成 MP4 容器的封装。

python
1import time
2
3def start_recording(serial: str, remote: str = "/sdcard/run.mp4") -> subprocess.Popen:
4 return subprocess.Popen(
5 ["adb", "-s", serial, "shell", "screenrecord", remote]
6 )
7
8def stop_recording(
9 serial: str,
10 proc: subprocess.Popen,
11 remote: str = "/sdcard/run.mp4",
12 local: str = "run.mp4",
13) -> None:
14 # 在设备上发送 SIGINT 使 screenrecord 正确关闭文件
15 subprocess.run(["adb", "-s", serial, "shell", "pkill", "-SIGINT", "screenrecord"])
16 proc.wait(timeout=10)
17 time.sleep(1) # 给设备一点时间完成容器封装
18 subprocess.run(["adb", "-s", serial, "pull", remote, local], check=True)

5. 使用 FFmpeg 组装视频报告

不同设备有不同的屏幕分辨率,因此不能直接用 -c copy 拼接。我们将每个片段标准化为通用格式(1080×1920),并通过 drawtext 叠加序列号。之后所有片段都相同,最终组装时使用快速拼接,无需重新编码。

python
1def label_clip(src: str, dst: str, label: str) -> None:
2 """缩放至 1080x1920 并叠加标签(设备序列号)。"""
3 vf = (
4 "scale=1080:1920:force_original_aspect_ratio=decrease,"
5 "pad=1080:1920:(ow-iw)/2:(oh-ih)/2,"
6 f"drawtext=text='{label}':x=20:y=20:fontsize=42:"
7 "fontcolor=white:box=1:boxcolor=black@0.6"
8 )
9 subprocess.run(
10 ["ffmpeg", "-y", "-i", src, "-vf", vf,
11 "-an", "-c:v", "libx264", "-preset", "veryfast", "-crf", "23", dst],
12 check=True,
13 )
14
15def concat_report(clips: list[str], out: str = "test_report.mp4") -> None:
16 with open("concat_list.txt", "w") as f:
17 for c in clips:
18 f.write(f"file '{c}'\n")
19 subprocess.run(
20 ["ffmpeg", "-y", "-f", "concat", "-safe", "0",
21 "-i", "concat_list.txt", "-c", "copy", out],
22 check=True,
23 )

6. 整合所有步骤

python
1def main() -> None:
2 apk = "app-debug.apk"
3 package = "com.example.app.test"
4 runner = "androidx.test.runner.AndroidJUnitRunner"
5
6 serials = get_devices()
7 if not serials:
8 print("未找到设备。请检查 USB 连接和 'adb devices' 的输出。")
9 return
10
11 print(f"找到设备数量:{len(serials)}")
12 install_on_all(apk, serials)
13
14 labeled: list[str] = []
15 for serial in serials:
16 proc = start_recording(serial)
17 _, ok = run_instrumented_tests(serial, package, runner)
18 stop_recording(serial, proc, local=f"{serial}.mp4")
19 print(f"[{'PASS' if ok else 'FAIL'}] 设备 {serial} 上的测试")
20
21 out = f"{serial}_labeled.mp4"
22 label_clip(f"{serial}.mp4", out, serial)
23 labeled.append(out)
24
25 concat_report(labeled, "test_report.mp4")
26 print("完成:test_report.mp4")
27
28if __name__ == "__main__":
29 main()

这里"录制 + 测试"循环按顺序在设备间执行 -> 这样更易读。对于真正的设备集群,你可能也想将此块包装在 ThreadPoolExecutor 中,以便所有设备同时测试;逻辑与第 2 节的安装相同。

不要重复造轮子:现成的工具

Ridark - inline image
  • scrcpy -> 从电脑实时镜像和控制设备。调试失败测试时不可或缺。
  • Appium / Espresso / UI Automator -> 成熟的 UI 测试框架;上面的 am instrument 就是它们的引擎。
  • Gradle Managed Devices -> 直接从构建中在模拟器上运行测试,无需手动操作 ADB。
  • Firebase Test Lab / AWS Device Farm -> 如果你不想自己维护硬件,可以使用云端的真实设备集群。
  • GNU parallel -> 如果你更愿意用 bash 而不是 Python 来编排。

经济账:成本与节省

测试自动化不是"凭空赚钱" -> 而是削减两个最昂贵的项目:人工工时云端分钟数。以下是三种典型规模下的估算。数字仅供参考,取决于地区、设备供应商和提供商定价,购买前请查看当前费率。

自有设备集群 -> 一次性投资

项目

10 台设备

30 台设备

50 台设备

二手 Android 手机(约 $60/台)

~$600

~$1,800

~$3,000

供电 USB 集线器

~$100

~$250

~$400

迷你 PC / 主机

~$400

~$400

~$500

线缆、机架、杂项

~$80

~$150

~$250

一次性总计

~$1,200

~$2,600

~$4,150

每月电费

几美元

~$10–20

~$20–40

这是资本支出:一次性投入,设备集群可以运行数年,几乎零成本。

云端 -> 按分钟付费

Firebase Test Lab、AWS Device Farm、BrowserStack 等按设备分钟收费 -> 大约 $0.05–0.20/设备分钟。一次在 30 台设备上各运行 5 分钟的回归测试,就是 150 设备分钟,即每次约 $7.5–30。

现在乘以 CI 强度:

运行频率

每月运行次数

费用(按每次 ~$15 计算)

每天 2 次

~44

~$660/月

每天 10 次

~220

~$3,300/月

每次推送(活跃团队)

500+

$7,500+/月

盈亏平衡点: 一个 30 台设备的集群(~$2,600)在每天运行 2 次的适度频率下,大约 4 个月就能回本;在活跃 CI 下,不到一个月。之后云端的账单每月持续产生,而设备集群则没有。

人工 -> 释放的时间

在 30 台设备上手动运行一次回归测试场景,大约需要 一名 QA 工程师的一个工作日。每周运行两次回归测试,每月累计约 8 个人天。以 QA 成本约 $1,600–2,700/月计算,这相当于管道释放出的相当一部分薪资,用于更有意义的工作,而不是重复"连接–安装–点击–录制"×30 的繁琐劳动。

如何转化为收益

这里没有"从脚本直接赚钱"的方式 -> 但有三种间接但非常真实的机制:

  • 更快的发布。 回归测试从一天缩短到几分钟 → 你更频繁地发布功能 → 更快响应市场。对于订阅产品,这直接关系到留存和收入。
  • 更少的生产环境缺陷。 在发布前捕获特定三星设备上的崩溃,成本极低;同样的崩溃到达用户手中,意味着应用商店评分下降、用户流失和退款。每个早期捕获的 bug 都是几篇永远不会出现的负面评论。
  • 它可以作为服务出售。 搭建设备集群和 CI 测试是自由职业和外包中需求旺盛的角色。上面的管道就是此类服务的现成核心。

与"流量农场"方案的关键区别:在那里,钱来自欺骗算法,最终以封号告终。在这里,钱来自节省的工时和避免的损失。前者会崩溃;后者是一个可持续的商业案例,你可以自豪地展示给客户。

总结

结果是一个可复现的管道:一个命令,应用就能在整个设备集群上运行测试,最终生成一个视频报告,显示每台设备上的具体行为。

收藏并订阅,以免错过能帮你省钱的实用文章 📝

一键保存

使用 YouMind AI 深度阅读爆款文章

保存原文、追问细节、总结观点,并在一个 AI 工作空间里把爆款文章沉淀成可复用笔记。

了解 YouMind
写给创作者

把你的 Markdown 变成干净的 𝕏 文章

图片上传、表格、代码块,往 𝕏 上手动重排太痛苦。YouMind 把整篇 Markdown 一键转成干净、可直接发布的 𝕏 文章草稿。

试试 Markdown 转 𝕏

更多可拆解样本

近期爆款文章

探索更多爆款文章