在云端 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 只代表调试通道已建立。流水线还应轮询 sys.boot_completed,确认返回 1 后再安装应用或启动测试。

多个任务可以共用同一个 AVD 目录吗?

不建议。并发任务应使用独立的 AVD 副本、端口和数据目录,否则容易发生锁文件冲突、快照互相覆盖和测试状态污染。

独享物理节点

把下一次构建放到独享云端 Mac 上运行

选择机型、节点与计费周期。配置和美元金额在下单前完整列明,可用状态以控制台实时返回为准。

选择方案并下单