当你的应用需要在几十台真实手机、不同厂商、不同 Android 版本和屏幕分辨率的"动物园"中运行时,手动测试很快就会变成一场噩梦。下面是一个 Python 管道,它能自动发现所有连接的设备,在所有设备上并行安装 APK,运行插桩测试,录制每台设备的运行视频,并用 FFmpeg 将这些录像拼接成一个视频报告。
整个技术栈 > Python + ADB + FFmpeg < 是标准的 QA 工具。没有魔法,只是把繁琐的工作自动化了。

管道架构
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. 设备发现

adb devices 也会列出未授权/离线状态的设备。我们只保留实际处于"device"状态的设备。
1import subprocess23def get_devices() -> list[str]:4 """返回所有处于 'device' 状态的设备序列号。"""5 out = subprocess.run(6 ["adb", "devices"],7 capture_output=True, text=True, check=True,8 ).stdout910 serials: list[str] = []11 for line in out.splitlines()[1:]: # 第一行是 "List of devices" 标题12 line = line.strip()13 if line.endswith("\tdevice"): # 排除 unauthorized / offline14 serials.append(line.split("\t")[0])15 return serials
2. 并行 APK 安装
在 50 台设备上一台接一台地安装很慢。我们将任务分散到线程池中:每个 adb install 都是一个独立的进程,因此线程在这里效果很好(我们等待的是 I/O,而不是消耗 CPU)。
1from concurrent.futures import ThreadPoolExecutor, as_completed23def 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.stdout9 return serial, ok, (r.stdout + r.stderr).strip()1011def 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 -> 我们据此判断结果。
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 == 012 return serial, ok
package 是测试包 ID,通常是 com.example.app.test。
4. 在测试期间录制屏幕
screenrecord 直接在设备上录制视频。需要注意的限制:每个文件约 3 分钟的上限,并且 没有音频。我们在后台开始录制,运行测试,然后干净地停止并拉取文件到主机。
停止录制最可靠的方法不是通过本地 adb 发送信号,而是在设备本身上使用 pkill -> 这样 screenrecord 能正确完成 MP4 容器的封装。
1import time23def start_recording(serial: str, remote: str = "/sdcard/run.mp4") -> subprocess.Popen:4 return subprocess.Popen(5 ["adb", "-s", serial, "shell", "screenrecord", remote]6 )78def 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 叠加序列号。之后所有片段都相同,最终组装时使用快速拼接,无需重新编码。
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 )1415def 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. 整合所有步骤
1def main() -> None:2 apk = "app-debug.apk"3 package = "com.example.app.test"4 runner = "androidx.test.runner.AndroidJUnitRunner"56 serials = get_devices()7 if not serials:8 print("未找到设备。请检查 USB 连接和 'adb devices' 的输出。")9 return1011 print(f"找到设备数量:{len(serials)}")12 install_on_all(apk, serials)1314 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} 上的测试")2021 out = f"{serial}_labeled.mp4"22 label_clip(f"{serial}.mp4", out, serial)23 labeled.append(out)2425 concat_report(labeled, "test_report.mp4")26 print("完成:test_report.mp4")2728if __name__ == "__main__":29 main()
这里"录制 + 测试"循环按顺序在设备间执行 -> 这样更易读。对于真正的设备集群,你可能也想将此块包装在 ThreadPoolExecutor 中,以便所有设备同时测试;逻辑与第 2 节的安装相同。
不要重复造轮子:现成的工具

- 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 测试是自由职业和外包中需求旺盛的角色。上面的管道就是此类服务的现成核心。
与"流量农场"方案的关键区别:在那里,钱来自欺骗算法,最终以封号告终。在这里,钱来自节省的工时和避免的损失。前者会崩溃;后者是一个可持续的商业案例,你可以自豪地展示给客户。
总结
结果是一个可复现的管道:一个命令,应用就能在整个设备集群上运行测试,最终生成一个视频报告,显示每台设备上的具体行为。





