Run an Android ARM64 Emulator on a Cloud Mac

Run an Android ARM64 Emulator on a Cloud Mac

When a mobile project includes both iOS and Android clients, teams often reserve a separate execution environment for Android checks. In fact, deploying an ARM64 emulator on an Apple Silicon cloud Mac keeps code checkout, API smoke tests, and cross-platform acceptance checks in the same pipeline. The most common problems are not caused by tool installation, but by choosing the wrong architecture, declaring startup complete too early, sharing state between concurrent jobs, and failing to preserve evidence for later investigation.

Pin the Architecture and Directories First

Before running workloads, the execution node should verify hardware virtualization support and standardize the locations of the Android SDK, AVDs, and build artifacts. Scripts should not depend on environment variables that are only temporarily available in an interactive 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 should report that virtualization is available, and emulator -accel-check should also pass. If the results differ, first verify that the commands are running under the same user context as the actual job instead of repeatedly reinstalling the SDK.

Apple Silicon nodes should use an arm64-v8a system image. An x86_64 image not only adds translation overhead but can also cause native library loading failures to be incorrectly attributed to application code. Pin the system image version with a repository variable and perform upgrades through merge requests rather than automatically following the latest version in every job.

Create a Reusable Base AVD

Install the platform, emulator, and image explicitly required by the project, then create a base device that does not depend on any user's interactive state.

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"

After creation, inspect config.ini. Continuous integration usually does not need a camera, microphone, or a large writable data partition. Disable unnecessary devices and pin the memory, screen density, and resolution. The fewer parameters involved, the easier the baseline is to reproduce.

The base AVD should only provide a clean template that has completed its first boot. Test data, login state, and application caches must not be written back to the base directory.

The first boot must complete system initialization. Once the home screen services are available, disable animations, remove temporary applications, and save the ci-base snapshot. The emulator version used to create the snapshot must match the version used to restore it. After upgrading the emulator, rebuild the snapshot instead of continuing to reuse the old one.

Headless Startup Must Check More Than ADB

Use -no-window to disable the graphical window in the pipeline, and assign a separate even-numbered port to each job. A device entry in adb devices only means that the transport channel has been established; it does not mean that the system has finished booting.

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

The startup wait must have an overall timeout. If it expires, preserve getprop, logcat, and the emulator's standard error before terminating the process. Waiting indefinitely only occupies an execution slot and hides corrupted images or port conflicts.

Disable Animations and Normalize State

Even after restoring a snapshot, apply a set of idempotent settings so that critical state is not omitted when the base image is rebuilt.

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

These commands do not replace test fixtures. Language, time zone, permissions, and network state should still be set explicitly by each test and restored afterward.

Build a Minimal Acceptance Loop with ADB

Once the emulator is ready, verify installation, launch, and process liveness before running the full test suite. This makes it possible to distinguish environment failures from failed business assertions.

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 returns the launch result and timing fields. The script should verify that the status indicates success and that pidof produces output. A zero exit status from the installation command alone does not prove that the entry Activity can be resolved, the process can start, or the native libraries use the correct architecture.

When a failure occurs, preserve at least the following evidence:

Evidence Command or location Purpose
Device properties adb shell getprop Verify the API, ABI, and boot state
System logs adb logcat -d -v threadtime Diagnose crashes, permission failures, and service errors
Installation details adb shell dumpsys package "$APP_ID" Verify the version, entry point, and ABI
Screen state adb exec-out screencap -p Identify overlays, dialogs, and black screens
Emulator output Job standard error file Identify snapshot and virtualization problems

Sanitize logs before archiving them so that environment variables, access tokens, or test account credentials do not enter long-lived pipeline artifacts.

Isolate Concurrent Jobs and Clean Up Reliably

When multiple emulators run on the same physical node, every job must have its own port, AVD copy, and temporary directory. Multiple processes must not open the same base AVD directly, because lock files, user data, and snapshots can overwrite one another.

At the start of a job, copy the base AVD into the working directory and rewrite the corresponding .ini path. Ports should be allocated by the scheduler and must remain even-numbered and unique. Whether the tests succeed or fail, always perform cleanup afterward:

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

Do not determine the concurrency limit from CPU core count alone. The emulators, applications, and build jobs all consume memory and disk bandwidth at the same time. A more reliable approach is to begin with a single instance, record peak memory usage, startup time, and test duration, and then increase concurrency gradually. If startup time and failure rate rise together, reduce concurrency by one level.

Reproducibility ultimately depends not on a particular launch flag, but on four boundaries: pinning the image version, verifying boot completion, isolating job state, and archiving failure evidence. Once these four requirements become part of the pipeline contract, the Android emulator can move from an ad hoc tool to a stable engineering execution unit.

Frequently asked questions

Why is the device state in adb devices not enough?

The device state only confirms that the ADB transport is available. Wait until sys.boot_completed returns 1 before installing the app or starting tests.

Can concurrent CI jobs share one AVD directory?

No. Give each job its own AVD copy, emulator port, and data directory to prevent lock conflicts, overwritten snapshots, and leaked test state.

Dedicated physical node

Run your next build on a dedicated cloud Mac

Choose your model, region, and billing cycle. Configuration details and USD pricing are shown in full before checkout; availability is confirmed live by the console.

Choose a plan and order