クラウド Mac で Android ARM64 エミュレータを動かす

クラウド Mac で Android ARM64 エミュレータを動かす

モバイルプロジェクトに iOS と Android の両クライアントが含まれる場合、Android のチェックだけを別の実行環境に分けるチームは少なくありません。しかし、Apple Silicon を搭載したクラウド Mac に ARM64 エミュレータを構築すれば、コードのチェックアウト、API のスモークテスト、両プラットフォームの受け入れ確認を同じパイプライン内で実行できます。問題になりやすいのはツールのインストールではなく、アーキテクチャの選択ミス、早すぎる起動完了判定、並行ジョブ間での状態共有、そして障害発生時に検証可能な証拠を残していないことです。

アーキテクチャとディレクトリを最初に固定する

実行ノードでは、まずハードウェア仮想化が利用できることを確認し、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 devicesdevice と表示されても、それは転送チャネルが確立されたことを示すだけで、システムの起動完了を意味するものではありません。

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 コア数だけで決めてはいけません。エミュレータ、アプリケーション、ビルドジョブは、メモリとディスク帯域を同時に消費します。より安定した方法は、まず単一インスタンスから始め、ピークメモリ使用量、起動時間、テスト所要時間を記録しながら、並行実行数を段階的に増やすことです。起動時間と失敗率が同時に上昇した場合は、並行実行数を一段階戻します。

最終的な再現性を左右するのは、特定の起動オプションではなく、4 つの境界です。すなわち、イメージのバージョンを固定すること、起動完了を検証できること、ジョブ間の状態を分離すること、障害発生時の証拠をアーカイブできることです。この 4 項目をパイプラインの契約として明文化すれば、Android エミュレータは一時的なツールではなく、安定したエンジニアリング実行単位になります。

よくある質問

adb devices に device と表示されたらテストを開始できますか?

まだ開始しません。これは ADB 接続の確立だけを示します。sys.boot_completed が 1 を返すまで待ってからアプリの導入とテストを実行します。

複数のジョブで同じ AVD を共有できますか?

共有は避けます。ジョブごとに AVD のコピー、ポート、データディレクトリを分け、ロック競合とスナップショットの上書きを防ぎます。

専用物理ノード

次回のビルドを専用クラウドMacで実行

モデル、ノード、課金期間を選択できます。構成と米ドル価格は注文前にすべて表示され、利用可能状況はコンソールからリアルタイムで確認できます。

プランを選んで注文