移动端项目同时包含 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
启动等待必须设置总超时。若超时,应保存 getprop、logcat 和模拟器标准错误,然后终止进程。无限等待只会占住执行槽位,并掩盖镜像损坏或端口冲突。
关闭动画并校准状态
恢复快照后仍应执行一组幂等设置,避免基础镜像被重建时遗漏关键状态。
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 上运行
选择机型、节点与计费周期。配置和美元金额在下单前完整列明,可用状态以控制台实时返回为准。