使用 ADB + FFmpeg 打造 Android 裝置農場:實現 10–50 台裝置的自動化 UI 測試

@ridark_eth
英語1 天前 · 2026年7月07日
209K
104
10
22
175

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 Debug Bridge) 來自 Android Platform Tools -> 裝置控制。
  • Python 3.10+ -> 編排 (我使用 list[str]、tuple[...] 而不需要 from __future__)。
  • FFmpeg -> 影片處理與組裝。
  • 已啟用 USB 偵錯 的裝置,透過 USB 連接 (或透過 adb tcpip 經由 Wi-Fi 連接)。

所有程式碼貫穿一個原則:我將參數以 列表 形式傳遞給 subprocess,並且不使用 shell=True。這樣更安全 (不會因檔案名稱造成注入攻擊),也不會因路徑中的空格或特殊字元而中斷。

1. 裝置發現

Ridark - inline image

adb devices 也會列出處於 unauthorized / offline 狀態的裝置。我們只保留實際處於 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:[email protected]"
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 的「連接–安裝–點擊–記錄」苦工。

這如何轉化為金錢

這裡沒有「來自腳本」的直接收入 -> 但有三種間接且非常真實的機制:

  • 更快的發布。 回歸測試從一天縮短到幾分鐘 → 你可以更頻繁地發布功能 → 更快地回應市場。對於訂閱制產品,這直接關係到留存率和收入。
  • 更少的生產環境錯誤。 在發布前捕獲特定 Samsung 機型的崩潰只需幾美分;同樣的崩潰影響到用戶則意味著商店評分下降、用戶流失和退款。每個及早捕獲的錯誤都意味著一些永遠不會出現的負面評論。
  • 它可以作為服務出售。 建立裝置農場和 CI 測試是自由職業和外包領域中需求旺盛的角色。上述管道是此類服務的現成核心。

與「互動農場計畫」的關鍵區別:在那裡,收入來自欺騙演算法,最終以封號告終。在這裡,收入來自節省的工時和避免的損失。前者會崩潰;後者是一個可持續的商業案例,你可以自豪地向客戶展示。

總結

結果是一個可重現的管道:一個命令,應用程式就能在整個裝置機群上進行測試,最終得到一個影片報告,顯示每台特定裝置上的行為。

收藏並訂閱,以免錯過能省錢的實用文章 📝

一鍵儲存

使用 YouMind AI 深度閱讀爆款文章

保存原文、追問細節、總結觀點,並在一個 AI 工作空間裡把爆款文章沉澱成可複用筆記。

了解 YouMind
寫給創作者

把你的 Markdown 變成乾淨的 𝕏 文章

圖片上傳、表格、程式碼區塊,往 𝕏 上手動重排太痛苦。YouMind 把整篇 Markdown 一鍵轉成乾淨、可直接發佈的 𝕏 文章草稿。

試試 Markdown 轉 𝕏

更多可拆解樣本

近期爆款文章

探索更多爆款文章