在雲端 Mac 上執行 Android ARM64 模擬器

在雲端 Mac 上執行 Android ARM64 模擬器

當行動專案同時包含 iOS 與 Android 用戶端時,團隊經常會把 Android 檢查留給另一套執行環境。其實,只要在 Apple Silicon 雲端 Mac 上部署 ARM64 模擬器,就能讓程式碼簽出、介面冒煙測試與雙端驗收都留在同一條流水線中。真正容易出錯的通常不是工具安裝,而是選錯架構、過早判定啟動完成、並行工作共用狀態,以及失敗後未留下可供複查的證據。

先固定架構與目錄

執行節點應先確認硬體虛擬化能力,再統一 Android SDK、AVD 與建置產物的位置。不要讓指令碼依賴僅在互動式 Shell 中暫時生效的環境變數。

export ANDROID_HOME="$HOME/Library/Android/sdk"
export ANDROID_AVD_HOME="$HOME/.android/avd"
export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"

sysctl kern.hv_support
emulator -accel-check
adb version

kern.hv_support 應回傳可用狀態,emulator -accel-check 也應順利通過。若兩者結果不同,應先確認命令是否確實在執行工作的使用者環境中執行,而不是直接反覆重新安裝 SDK。

Apple Silicon 節點應選用 arm64-v8a 系統映像檔。使用 x86_64 映像檔不僅會增加轉譯成本,也可能讓原生程式庫載入問題被誤判為應用程式碼錯誤。系統映像檔版本應由儲存庫變數固定,升級則透過合併請求完成,而不是在每次工作中自動追隨最新版本。

建立可重複使用的基礎 AVD

先安裝專案明確需要的平台、模擬器與映像檔,再建立不綁定個人互動狀態的基礎裝置。

API_LEVEL=35
IMAGE="system-images;android-${API_LEVEL};google_apis;arm64-v8a"
AVD_NAME="ci-arm64-api-${API_LEVEL}"

sdkmanager "platform-tools" "emulator" "platforms;android-${API_LEVEL}" "$IMAGE"
printf "no\n" | avdmanager create avd \
  --force \
  --name "$AVD_NAME" \
  --package "$IMAGE" \
  --device "pixel_6"

建立完成後應檢查 config.ini。持續整合通常不需要攝影機、麥克風與大容量可寫入資料磁碟,因此可以停用無關裝置,並固定記憶體、螢幕密度與解析度。參數越少,基準環境就越容易重現。

基礎 AVD 只負責提供已完成首次啟動的乾淨範本。測試資料、登入狀態與應用程式快取不應寫回基礎目錄。

首次啟動時需要完成系統初始化。確認桌面服務可用後,關閉動畫、清除暫存應用程式,再儲存 ci-base 快照。產生快照與還原快照時使用的模擬器版本應保持一致;升級模擬器後應重新製作快照,而不是繼續沿用舊快照。

無介面啟動不能只等待 ADB

在流水線中使用 -no-window 關閉圖形視窗,並為每項工作分配獨立的偶數連接埠。adb devices 顯示 device 只代表傳輸通道已建立,並不表示系統已完成啟動。

AVD_NAME="ci-arm64-api-35"
EMULATOR_PORT=5556
SERIAL="emulator-${EMULATOR_PORT}"

emulator "@${AVD_NAME}" \
  -no-window \
  -no-audio \
  -no-boot-anim \
  -port "$EMULATOR_PORT" \
  -snapshot ci-base \
  -no-snapshot-save &

adb -s "$SERIAL" wait-for-device

for attempt in $(seq 1 90); do
  status="$(adb -s "$SERIAL" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')"
  [ "$status" = "1" ] && break
  sleep 2
done

[ "$status" = "1" ] || exit 1

等待啟動時必須設定總逾時。若發生逾時,應儲存 getproplogcat 與模擬器標準錯誤輸出,再終止程序。無限等待只會占用執行槽位,並掩蓋映像檔損壞或連接埠衝突。

關閉動畫並校準狀態

還原快照後仍應執行一組冪等設定,避免重建基礎映像檔時遺漏關鍵狀態。

adb -s "$SERIAL" shell settings put global window_animation_scale 0
adb -s "$SERIAL" shell settings put global transition_animation_scale 0
adb -s "$SERIAL" shell settings put global animator_duration_scale 0
adb -s "$SERIAL" shell input keyevent 82

這些命令不能取代測試夾具。語言、時區、權限與網路狀態仍應由測試案例明確設定,並在結束後還原。

使用 ADB 建立最小驗收閉環

模擬器準備完成後,應先驗證安裝、啟動與程序存活狀態,再進入完整測試套件。如此便能區分環境故障與業務斷言失敗。

adb -s "$SERIAL" install -r "$APK_PATH"
adb -s "$SERIAL" shell am force-stop "$APP_ID"
adb -s "$SERIAL" shell am start -W -n "${APP_ID}/${LAUNCH_ACTIVITY}"
adb -s "$SERIAL" shell pidof "$APP_ID"

am start -W 會回傳啟動結果與耗時欄位。指令碼應檢查狀態是否成功,同時確認 pidof 有輸出。僅憑安裝命令回傳零,無法證明入口 Activity 能夠解析、程序可以啟動,或原生程式庫架構正確。

失敗時建議保留下列最小證據集:

證據 命令或位置 用途
裝置屬性 adb shell getprop 核對 API、ABI 與啟動狀態
系統日誌 adb logcat -d -v threadtime 定位當機、權限與服務異常
安裝資訊 adb shell dumpsys package "$APP_ID" 核對版本、入口與 ABI
螢幕狀態 adb exec-out screencap -p 判斷遮擋、彈出視窗與黑畫面
模擬器輸出 工作標準錯誤檔案 識別快照與虛擬化問題

封存日誌前應先進行去識別化處理,避免環境變數、存取權杖或測試帳號憑證進入長期保存的流水線產物。

隔離並行工作並可靠清理

同一實體節點執行多個模擬器時,每項工作都必須擁有獨立的連接埠、AVD 副本與暫存目錄。不能讓多個程序直接開啟同一個基礎 AVD,否則鎖定檔、使用者資料與快照可能互相覆寫。

可以在工作開始時將基礎 AVD 複製到工作目錄,並改寫對應的 .ini 路徑。連接埠由排程器分配,必須保持為偶數且不可重複。測試結束後,無論成功或失敗,都應執行清理:

cleanup() {
  adb -s "$SERIAL" emu kill >/dev/null 2>&1 || true
  wait "$EMULATOR_PID" 2>/dev/null || true
  rm -rf "$JOB_AVD_HOME"
}
trap cleanup EXIT INT TERM

並行上限不能只依 CPU 核心數決定。模擬器、應用程式與建置工作會同時占用記憶體及磁碟頻寬。更穩妥的方式是從單一執行個體開始,記錄記憶體峰值、啟動時間與測試耗時,再逐步提高並行數;一旦啟動時間與失敗率同步上升,就應調降一級。

最終決定流程能否重現的關鍵,不是某個啟動參數,而是四項邊界:映像檔版本固定、啟動完成可驗證、工作狀態彼此隔離,以及失敗現場能夠封存。將這四項寫入流水線契約後,Android 模擬器才會從臨時工具轉變為穩定的工程執行單元。

常見問題

模擬器出現在 adb devices 後可以立即測試嗎?

不可以。device 只表示 ADB 通道已連線,還要輪詢 sys.boot_completed,確認回傳 1 後才能安裝應用程式或啟動測試。

多個持續整合工作可以共用同一個 AVD 嗎?

不建議。每個工作都應使用獨立的 AVD 副本、連接埠與資料目錄,避免鎖定衝突、快照互相覆寫及測試狀態污染。

獨享實體節點

將下一次建置放到獨享雲端 Mac 上執行

選擇機型、節點與計費週期。下單前會完整列出設定與美元金額,實際可用狀態以控制台即時回傳為準。

選擇方案並下單