Android 模拟器不必困在桌面:用 serve-avd 把调试画面、输入和日志收进一个浏览器端点
做 Android 调试时,模拟器通常是一个“看得见却不容易交给别人”的窗口。开发者要在本机看画面、点按控件、读 logcat;测试同事或远程机器上的自动化流程又需要同一台设备的状态。传统做法往往是在屏幕共享、远程桌面、截图脚本和零散的 adb 命令之间切换。它们都能解决一小段问题,但很难让画面、输入、UI 结构和事件记录落在同一个可访问边界里。
serve-avd 是一个 Apache-2.0 许可证的 Node.js 工具,定位是“Android Emulator 的 npx serve”。它连接正在运行的 Android Emulator(大多数通过 adb 连接的真机也可用),将画面流、浏览器交互、logcat、截图与 UI hierarchy 暴露为本地 HTTP 和 WebSocket 服务。重点不是把模拟器搬到公网上,而是把已有调试设备变成一个默认绑定本机回环地址、可被浏览器和脚本共同使用的调试端点。
本文以本地开发为主。不要把它理解为云真机平台,也不要把未鉴权的端口直接暴露到互联网:它提供的是对设备屏幕和输入的控制能力,访问范围必须和调试环境的权限模型一起设计。
先确认边界:它复用 adb,而不是给 App 注入 SDK
serve-avd 的工作链路很直接:它通过 adb screenrecord --output-format=h264 取得模拟器屏幕,再将视频重封装为浏览器可解码的 H.264 AVCC 流;浏览器不能使用 WebCodecs 时,服务会回退到 MJPEG 截图流。输入则经由持久的 adb shell 发送触摸、按键和文本命令。这个设计意味着你的应用无需接入额外 SDK、无需 root、也无需为测试版加入埋点。
工具同时把设备状态、事件、日志和无障碍层级放到同一服务中。浏览器侧可以点击、拖动、滚动、切换 Home/Back 等系统按钮;命令行可以保存截图、获取前台 Activity,或以 JSON 形式导出界面结构。对“先让人看,再让自动化读取”的排障流程而言,这比单独上传几张截图更可追溯。
代价也要说清楚。它依赖 Android SDK platform-tools 中的 adb;若想按 AVD 名称启动模拟器,还需要 emulator 二进制。README 要求 Node.js 18.17 或更高的维护中 LTS 版本。type 子命令受 Android input text 的限制,只支持 ASCII;多指缩放还要求可 root 的非 Play 镜像。因此它适合把现有设备的可见性和可操作性统一起来,不承诺替代所有真机、输入法或多点触控测试。
从一台本地模拟器开始
先启动 Android Emulator,确保 adb devices 能看到它。安装依赖满足后,直接运行:
npx serve-avd
不带设备参数时,serve-avd 会附加所有在线设备;如果没有在线设备,它会尝试启动第一个 AVD。想明确控制目标,传入 adb serial 或 AVD 名称即可:
npx serve-avd emulator-5554 npx serve-avd Pixel_9_Pro_XL
第二条命令中的名称是 AVD 名,工具会在需要时启动它。多设备并行时可以一次传多个 serial;这比在多台模拟器窗口之间反复切换更适合复现“只有某个 API level 或某个账号状态失败”的问题。
默认服务监听 127.0.0.1,预览端口为 3200。只有在明确需要同一局域网的受控设备访问、并已配置网络边界时,才考虑 --host 0.0.0.0。不要把这个参数当成远程协作的默认选项:浏览器端点能接收输入,暴露范围扩大后,风险也随之扩大。
把手工观察变成可复现的证据
浏览器画面适合确认“现在到底发生了什么”,但排障记录还应留下机器可读的证据。serve-avd 的 CLI 已提供几个很小却实用的原子操作:
serve-avd screenshot ./failure.png serve-avd foreground serve-avd ax | jq '.root.children[0]' serve-avd event-log --json
这里的关键不是让脚本“猜”界面,而是把截图、前台 Activity、层级树和事件日志一并保存在失败报告中。比如一次登录流程在模拟器上卡住,先保存屏幕,再记录前台 Activity 和 ax 输出;随后才决定是网络、权限弹窗、焦点丢失还是页面状态的问题。这样另一个开发者无需先接管你的桌面,也能从同一组证据开始复现。
坐标输入采用归一化的 0 到 1 范围,因此一个简单的点击可以写为:
serve-avd tap 0.5 0.9
serve-avd gesture '{"type":"begin","x":0.5,"y":0.8}'
serve-avd gesture '{"type":"move","x":0.5,"y":0.4}'
serve-avd gesture '{"type":"end","x":0.5,"y":0.3}'
这些命令适合辅助复现,不该替代更完整的测试框架。特别是依赖复杂文本输入、系统级权限或 Play 镜像多点触控的场景,应先验证设备限制,再把命令写进 CI 或回归脚本。
一次故障复现可以怎样留档
把工具接入日常排障时,建议给每次失败定义一个很小的证据包,而不是只在聊天里贴一句“模拟器卡住了”。例如,在测试脚本的失败分支中依次保存截图、foreground 输出、ax 的原始 JSON 和最近事件;文件名带上构建号、设备 serial 与时间戳。这样同一份工件既能让开发者在浏览器里回看,也能让负责 UI 自动化的人检查节点层级是否发生变化。
还应当区分“触发操作”和“观察结果”。tap、gesture 与 type 是触发操作,适合记录为可重放步骤;截图、前台 Activity、logcat 和层级树才是结果证据。若测试失败后只保留点击坐标,很难知道失败是控件不存在、网络响应慢,还是界面跳转到了预期之外的 Activity。反过来,只保存图片也很难知道前置动作。把两者成对保存,才能在升级 Android 镜像或修改 UI 后判断差异来自哪里。
对于多设备回归,不必一开始就让所有设备长期常驻服务。先以 serve-avd --list 检查现有流,再在一轮任务结束后使用 serve-avd --kill 回收会话;对于由 CI 启动的进程,则把清理动作放在无论成功或失败都会执行的 finally/cleanup 阶段。这样既能避免端口和 adb 捕获残留,也不会把一次临时排障变成长期占用资源的后台服务。
把预览嵌进已有开发服务器,而不是再开一个孤岛
如果团队已有 Expo/Metro、Vite、Next 或 Express 开发服务器,serve-avd 还提供 serve-avd/middleware。它是 Connect 风格中间件,可将预览界面挂载到已有服务的路径下:
import { emuMiddleware } from "serve-avd/middleware";
const middleware = emuMiddleware({ basePath: "/.emu" });
app.use(middleware);
const server = app.listen(3000);
server.on("upgrade", (req, socket, head) => {
middleware.handleUpgrade(req, socket, head);
});
最终预览页面位于 /.emu,状态 JSON 位于 /.emu/api。关键的一行是 upgrade 转发:实时输入和视频流需要 WebSocket 升级;漏掉它可能得到一个能打开、却不能正常交互的页面。服务停止时,官方 README 建议调用导出的 closeAllDeviceSessions(),让 adb 捕获进程一起结束,避免测试进程退出后留下无主会话。
这种嵌入模式的价值在于同源。画面、状态、WebSocket 和开发服务器可从一个端口进入;在受控的反向代理或隧道环境中,也能减少分别转发多个端口的配置。但“同源”不等于“自动安全”:如果你在远程环境中转发它,仍要由现有的认证、访问控制和 TLS 终止层负责限制访问者。
适合谁,以及什么时候先不要上
serve-avd 很适合本地或受控远程环境中的 Android 开发:你需要在浏览器中观察模拟器,想把 logcat、截图、UI 层级和输入操作集中到一个端点,或希望为脚本和人工排障提供一致的设备视图。它也适合先在本机验证远程模拟器方案,再决定是否值得采购或搭建完整的托管设备基础设施。
但如果目标是面向外部测试者的公共设备云、跨大量机型的并发调度,或者需要完整的身份隔离与审计体系,应选择专门的设备测试平台或在外围补齐这些能力。最稳妥的落地顺序是:先只绑定本机,观察一台无敏感数据的 AVD;再把截图、foreground、ax 与 event-log 接进失败收集;确认会话清理和访问控制后,才考虑将预览嵌入团队的开发服务。
把模拟器从“某个人桌面上的黑盒窗口”变成一组可读取、可回放的调试证据,并不要求重写应用。对多数日常问题而言,这种低侵入的统一入口,已经足以缩短发现、复现和定位之间的距离。