Playwright Android 自动化完全指南:AndroidDevice API 详解与 ADB 驱动实现原理
Playwright 自 v1.9 起提供了实验性的 Android 自动化支持,其核心抽象是 AndroidDevice 类:它代表一台通过 ADB 连接的真实设备或模拟器(AVD),既能对原生 UI 控件做点击、滑动、填写等交互,也能接管设备上的 Chrome 浏览器与 WebView,让同一套 Page / BrowserContext API 直接运行在移动端。读完本篇,你将掌握 AndroidDevice 的全部方法签名与参数默认值、从 ADB 连接到浏览器接管的完整实战流程,以及从源码层面看懂 Playwright 如何在设备上安装驱动 APK、通过本地抽象 socket 与 WebView 建立 DevTools 通道的底层机制。
一、AndroidDevice 是什么:概念、前提与获取方式
根据官方 API 文档 class-androiddevice.md,AndroidDevice 表示一台已连接的设备(真机或模拟),可通过 method: Android.devices 获取。它继承自事件源,核心事件有两个:
| 事件 | 载荷 | 触发时机 |
|---|---|---|
close |
AndroidDevice |
设备连接关闭时(v1.28 起) |
webView |
AndroidWebView |
检测到新的 WebView 实例时 |
运行 Android 自动化前需要满足以下前提(引自 class-android.md):
- 一台 Android 真机或 AVD 模拟器;
- 正在运行并与设备完成认证的 ADB daemon——通常执行一次
adb devices即可; - 设备上安装了 Chrome 87 或更高版本;
- 在 Chrome 的
chrome://flags中开启 "Enable command line on non-rooted devices"(这是 Playwright 能给 Chrome 传--remote-debugging-socket-name等启动参数的前提)。
设备发现由 Android.devices() 完成,支持指定远程 ADB server 的 host(默认 127.0.0.1)与 port(默认 5037),以及 omitDriverInstall(跳过每次连接时自动安装驱动 APK,见第五节)。多设备场景下还可以用 Android.connect(endpoint)(v1.28 起)连接到 Android.launchServer() 启动的服务器实例;launchServer 的 WebSocket 默认只监听 localhost,且 wsPath 默认是一个不可猜测的随机串——文档明确警告:任何知道 wsPath 的进程都可能接管 OS 用户权限,因此显式指定 wsPath 时必须使用不可猜测的 token。
设备连接建立后的第一步通常是最基础的三个只读方法:
const { _android: android } = require('playwright');
(async () => {
// 获取所有已连接的 Android 设备
const [device] = await android.devices();
console.log(`Model: ${device.model()}`); // 设备型号
console.log(`Serial: ${device.serial()}`); // 设备序列号
await device.screenshot({ path: 'device.png' }); // 整机截图
await device.close();
})();
model()返回设备型号。从源码 android.ts 看,它在设备初始化时执行shell:getprop ro.product.model获取,因此这是真实的系统属性值。serial()返回设备序列号,即 ADB 层识别设备的唯一标识。screenshot()返回截图Buffer,可选path参数落盘(相对路径基于当前工作目录)。服务端实现就是执行shell:screencap -p(见 android.ts),所以截图是 PNG 格式且覆盖整个屏幕。文档同时提醒:设备必须处于唤醒状态才能出图,建议开启开发者模式的 "Stay awake"。
二、AndroidDevice 完整 API 参考
以下按功能域整理 class-androiddevice.md 中的全部方法。所有标注 timeout 的方法共享同一套超时语义:默认 30 秒,可用 device.setDefaultTimeout(ms) 修改(该设置在设备对象级别,会覆盖 Android.setDefaultTimeout 的全局默认值),传 0 关闭超时。
2.1 控件交互方法(都接收 AndroidSelector)
AndroidSelector 是匹配原生控件的选择器对象,字段包括:res(资源 id)、text、desc(content description)、pkg、clazz、checkable / checked / clickable / enabled / focusable / focused / longClickable / scrollable / selected 等布尔状态、depth,以及结构查询 hasChild: { selector } 与 hasDescendant: { selector, maxDepth }。字符串形式的字段值在客户端会被编译为正则(详见第六节)。
| 方法 | 签名要点 | 说明 |
|---|---|---|
tap(selector, opts?) |
opts: { duration?, timeout? } |
点击控件。duration(毫秒)为可选按压时长 |
longTap(selector, opts?) |
opts: { timeout? } |
长按控件 |
fill(selector, text, opts?) |
opts: { timeout? } |
清空并填入文本,目标须是输入框 |
press(selector, key, opts?) |
key: AndroidKey |
在控件上下文中按键。客户端实现是 tap(selector) 后调用 input.press(key)(见 android.ts) |
swipe(selector, direction, percent, opts?) |
`direction: "down" | "up" |
scroll(selector, direction, percent, opts?) |
同上 | 滚动控件(作用于可滚动元素) |
fling(selector, direction, opts?) |
opts: { speed?, timeout? } |
快速甩动控件,speed 单位是像素/秒 |
drag(selector, dest, opts?) |
dest: { x, y } |
将控件拖拽到目标坐标点 |
pinchOpen(selector, percent, opts?) |
opts: { speed?, timeout? } |
按"放大"方向捏合,percent 为相对控件尺寸的比例 |
pinchClose(selector, percent, opts?) |
同上 | 按"缩小"方向捏合 |
除 drag(目标是绝对坐标 {x, y})外,其余方法都以 AndroidSelector 定位控件;speed(像素/秒)可选参数决定手势速度,缺省时由驱动端使用默认速度。
2.2 等待与查询
| 方法 | 签名要点 | 说明 |
|---|---|---|
wait(selector, opts?) |
opts: { state?: 'gone', timeout? } |
等待控件出现;state: 'gone' 时等待控件消失 |
info(selector) |
返回 AndroidElementInfo |
返回控件的文本、描述、资源 id、包名、类名等属性,是调试选择器的重要手段 |
waitForEvent(event, optionsOrPredicate?) |
事件名如 'webview' |
等待事件并传入谓词,谓词返回 truthy 时 resolve;默认超时 30000ms |
webViews() |
返回 AndroidWebView[] |
当前已打开的 WebView 列表 |
webView(selector, opts?) |
selector: { pkg?, socketName? } |
等待匹配 pkg 或 socketName 的 WebView 打开并返回 AndroidWebView。客户端实现(android.ts)是:先在本地已缓存的 WebView 集合中查找,找不到则挂起等待 webview 事件——事件由服务端每 500ms 轮询一次 Unix socket 列表产生(见第五节) |
2.3 设备级操作
| 方法 | 签名要点 | 说明 |
|---|---|---|
shell(command) |
返回 Buffer |
在设备上执行 shell 命令并返回输出。所有命令在服务端加 shell: 前缀经 ADB 执行 |
open(command) |
返回 AndroidSocket |
启动 shell 进程并返回可读写 socket(write / close,以及 data / close 事件),适合需要双向流的场景,如 open('localabstract:playwright_android_driver_socket') |
installApk(file, opts?) |
file: string | Buffer;opts: { args? } |
安装 APK,file 可以是本地路径或文件内容。args 是传给 cmd package install 的参数,默认 -r -t -S(覆盖安装、允许测试包、静默)。服务端实现通过 ADB socket 把 APK 字节流直接写入 cmd package install <args> <length> 通道(见 android.ts),无需先推文件 |
push(file, path, opts?) |
opts: { mode? } |
把文件拷贝到设备。mode 可选,默认 644(rw-r--r--)。从源码看它使用的是 ADB sync 协议:打开 sync: socket 后按 SEND / DATA(65535 字节分块)/ DONE 三段发送,并等待 OKAY 应答(见 android.ts) |
screenshot(opts?) |
opts: { path? },返回 Buffer |
整机截图,见第一节 |
launchBrowser(opts?) |
返回 BrowserContext |
在设备上启动 Chrome(或 pkg 指定的其他浏览器)并返回其持久化上下文。除 pkg 外,还接受标准 BrowserContext 参数(v1.8 起的共享 context 参数列表),以及 proxy、args(v1.29 起) |
close() |
— | 断开设备连接;触发 close 事件时也会清理所有已建立的浏览器连接 |
input |
属性,类型 AndroidInput |
低级输入通道:type(text)、press(key)、tap(point)、swipe(from, segments, steps)、drag(from, to, steps),直接以坐标/分段方式注入输入,不依赖控件选择器 |
setDefaultTimeout(timeout) |
毫秒 | 修改该设备下所有接受 timeout 的方法的默认超时 |
2.4 launchBrowser 的底层流程
launchBrowser() 是整个 Android API 中最复杂的调用。从服务端源码 android.ts 可以看到完整链路:
am force-stop <pkg>先杀掉目标浏览器(默认com.android.chrome);- 生成一个唯一的 socket 名
playwright_<guid>_devtools_remote(测试模式下为固定名webview_devtools_remote_playwright_test); - 组装 Chrome 启动参数:
--disable-fre、--no-default-browser-check、--remote-debugging-socket-name=<socketName>、Android 专用 Chromium 开关,以及用户传入的proxy(会翻译成--proxy-server/--proxy-bypass-list)与args; - 命令行的特殊字符容易在 shell 中出问题,所以源码把它 base64 编码后写入设备上的
/data/local/tmp/chrome-command-line,再用am start -a android.intent.action.VIEW -d about:blank <pkg>拉起浏览器(Chrome 的 command-line 文件机制会读取该文件); - 通过
open('localabstract:<socketName>')打开这个 DevTools 抽象 socket,手工完成一次 HTTP Upgrade 握手,把 socket 包装成AndroidBrowser(内置 WebSocket 收发器),随后以persistent持久化上下文模式连接CRBrowser,返回默认BrowserContext; - 成功后删除临时命令行文件;失败则关闭已建立的上下文并抛错。
这也解释了为什么文档要求开启 "Enable command line on non-rooted devices":Chrome 只有在允许命令行覆盖时才会读取注入的调试 socket 参数。
三、完整实战:从连接设备到自动化 Chrome 与 WebView
下面是官方文档(class-android.md)给出的端到端示例,覆盖了 model / serial / screenshot / shell / webView / fill / press / launchBrowser / close 等主力 API:
const { _android: android } = require('playwright');
(async () => {
// 连接设备。
const [device] = await android.devices();
console.log(`Model: ${device.model()}`);
console.log(`Serial: ${device.serial()}`);
// 对整个设备截图。
await device.screenshot({ path: 'device.png' });
{
// --------------------- WebView 自动化 -----------------------
// 启动一个带 WebView 的应用。
await device.shell('am force-stop org.chromium.webview_shell');
await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity');
// 获取 WebView。
const webview = await device.webView({ pkg: 'org.chromium.webview_shell' });
// 填充地址输入框。
await device.fill({
res: 'org.chromium.webview_shell:id/url_field',
}, 'github.com/microsoft/playwright');
await device.press({
res: 'org.chromium.webview_shell:id/url_field',
}, 'Enter');
// 像普通 Page 一样操作 WebView 里的页面。
const page = await webview.page();
await page.waitForURL(/.*microsoft\/playwright.*/);
console.log(await page.title());
}
{
// --------------------- Chrome 浏览器自动化 -----------------------
// 启动 Chrome。
await device.shell('am force-stop com.android.chrome');
const context = await device.launchBrowser();
// 像普通 BrowserContext 一样使用。
const page = await context.newPage();
await page.goto('https://webkit.org/');
console.log(await page.evaluate(() => window.location.href));
await page.screenshot({ path: 'page.png' });
await context.close();
}
// 关闭设备连接。
await device.close();
})();
注:示例中地址栏选择器
org.chromium.webview_shell:id/url_field演示了 Android 选择器的res字段用法;fill之后用press(..., 'Enter')提交。AndroidWebView.page()会把 WebView 适配为标准的Page对象(客户端实现见 android.ts,通过connectToWebView在 socket 上建立 DevTools 连接并取到上下文中的第一个页面),此后page.goto/page.title等 API 与桌面端完全一致。原文档示例中使用的page.waitForNavigation是旧版 API,新版可等价写作page.waitForURL。
AndroidWebView 对象本身也提供三个属性方法:pid()(宿主进程 PID)、pkg()(宿主包名)、以及内部使用的 socket 名;配合 device.webViews() 可以枚举当前所有 WebView,配合 device.on('webView', ...) 事件可以在应用内 WebView 打开的瞬间做出响应。
3.1 跨进程:launchServer / connect 模式
当 ADB 与测试进程不在同一台机器(例如 CI 节点连测试机上的 ADB server),v1.28 起的 server/client 模式更合适。服务端:
const { _android } = require('playwright');
(async () => {
const browserServer = await _android.launchServer({
// 多台设备连接、想固定使用其中一台时:
// deviceSerialNumber: '<deviceSerialNumber>',
});
const wsEndpoint = browserServer.wsEndpoint();
console.log(wsEndpoint);
})();
客户端:
const { _android } = require('playwright');
(async () => {
const device = await _android.connect('<wsEndpoint>');
console.log(device.model());
console.log(device.serial());
await device.shell('am force-stop com.android.chrome');
const context = await device.launchBrowser();
const page = await context.newPage();
await page.goto('https://webkit.org/');
console.log(await page.evaluate(() => window.location.href));
await page.screenshot({ path: 'page-chrome-1.png' });
await context.close();
})();
关键选项:launchServer 接受 adbHost / adbPort(指定 ADB server,默认 127.0.0.1:5037)、deviceSerialNumber(多设备时必须显式指定,否则抛错)、host(v1.45 起,默认 localhost,显式传 0.0.0.0 会把设备 RPC 暴露到网络)、port(默认 0,随机端口)、wsPath(默认不可猜测的随机串)和 omitDriverInstall。connect(endpoint, options?) 侧则支持 headers、slowMo(毫秒级减速,便于观察)与 timeout(默认 30000ms,0 表示禁用)。从客户端源码 android.ts 看,连接时会自动带上 x-playwright-browser: android 头,并在握手后校验 endpoint 是否由 launchServer 产生,不是则报 "Malformed endpoint"。
四、AndroidSelector 是怎么匹配的:客户端正则编译
AndroidDevice 的所有控件方法都接收 AndroidSelector,而它的字符串字段在发往服务端之前,会在客户端被统一编译成正则表达式(见 toSelectorChannel):
- 传入
RegExp时,直接取其source,即你写的正则原样生效; - 传入字符串时,所有正则特殊字符(
|\\{}()[\]^$+*?.)被转义,并整体包上^...$锚点——字符串值按"精确全匹配"处理,例如res: 'com.app:id/login_btn'只匹配该完整资源 id,而不是子串; hasChild/hasDescendant会递归做同样的编译,hasDescendant额外携带maxDepth限制向下搜索深度;- 布尔字段(
clickable、enabled、selected等)与depth则原样透传。
这个细节直接影响选择器编写:想"以 login 开头"要写 res: /^com\.app:id\/login/,而不是 res: 'com.app:id/login'。协议层的完整方法清单定义在 android.yml,可用于核对每个方法对应的 RPC 名称与参数。
五、源码深读:Playwright 如何"驱动"一台 Android 设备
理解了 API 表面之后,真正有趣的是服务端(packages/playwright-core/src/server/android/android.ts)如何在 ADB 之上构建出一套可靠的能力。
5.1 设备发现与 ADB 抽象
Android.devices() 调用后端 Backend.devices()(ADB 实现的 adb devices 语义),过滤出 status === 'device' 的条目,并按序列号增量维护一个 serial -> AndroidDevice 映射:新序列号创建设备对象,消失的序列号则从映射中移除(android.ts)。设备创建时会执行 shell:getprop ro.product.model 读型号。AndroidDevice 的 shell / screenshot / open 全部构建在两个后端原语上:
runCommand(command):执行shell:xxx形式的 ADB 命令并拿回输出;open(command):打开一条长连接 socket,例如shell:cmd package install ...、localabstract:<name>、sync:。
AndroidDevice.shell() 每次执行完命令后还会立即刷新一次 WebView 列表(_refreshWebViews),保证 am start 之类命令之后能尽快发现新 WebView。
5.2 驱动 APK:控件交互的真正执行者
tap / fill / swipe 等 UI 交互不直接走 ADB shell,而是走设备上安装的驱动 APK。首次需要交互时,_installDriver()(android.ts)会:
am force-stop com.microsoft.playwright.androiddriver停掉旧驱动;- 若未设置
omitDriverInstall,先cmd package uninstall两个驱动包(androiddriver与androiddriver.test),再从 Playwright 安装目录读取android-driver.apk与android-driver-target.apk,用与installApk相同的 socket 通道装上去(文件缺失时提示执行playwright install android); am instrument -w com.microsoft.playwright.androiddriver.test/androidx.test.runner.AndroidJUnitRunner启动 instrument 进程;- 轮询
localabstract:playwright_android_driver_socket直到可连接,包装成 JSON-RPC 式通道:每条消息是{ id, method, params },服务端按id匹配挂起的 Promise,error字段则触发 reject(见 _send)。
从源码结构看,UI 动作(tap、swipe、pinch 等)本质上是发给驱动 APK 的 RPC 调用,由 APK 内的 instrumentation 框架完成手势合成——这也意味着驱动 APK 版本与 Playwright 版本需要配套,默认每次连接都会重装以保证一致;CI 中确认驱动已就位时可用 devices({ omitDriverInstall: true }) 跳过这段安装开销。
5.3 WebView 是怎么被"看见"的
device.webView() / webViews() 的数据来自 _refreshWebViews()(android.ts):
- 每 500ms 执行
shell:cat /proc/net/unix | grep webview_devtools_remote,扫描系统 Unix socket 表; - 用正则提取
webview_devtools_remote_<pid>[<name>]形式的 socket 名,并从 socket 名中解析出宿主进程 PID,再用ps -A | grep <pid>反查出包名; - 与本地缓存比对:新出现的 socket 触发
webViewAdded事件(客户端即device.on('webView', ...)的来源),消失的触发webViewRemoved。
所以"检测到新 WebView"完全是对 /proc/net/unix 的轮询结果,webView({ pkg, socketName }) 的匹配键也由此而来;waitForEvent 的默认 30 秒超时覆盖了轮询发现的延迟。
5.4 连接链路与关闭语义
客户端 AndroidDevice 对象与设备之间是标准的 Playwright 通道协议(dispatcher 见 androidDispatcher.ts),而 close 的语义值得注意:device.close() 在服务端会停止 WebView 轮询、关闭所有浏览器连接(包括由 launchBrowser / WebView 建立的 DevTools 通道)、reject 所有未决的驱动 RPC、关闭驱动 socket 并断开 ADB 会话,最后向客户端广播 close 事件(android.ts)。在 connect() 模式下客户端还会把 close 与 WebSocket 连接绑定(_shouldCloseConnectionOnClose),设备掉线即断开 RPC 连接,避免悬挂状态。
六、已知限制、排错与延伸阅读
结合文档声明与源码行为,实践时的主要限制是:
- 必须有 ADB:原始 USB 通信尚不支持,一切能力都构建在 ADB daemon 之上(
adb devices是最基本的健康检查); - 截图要求设备唤醒:锁屏状态下
screenshot()可能失败或返回黑屏,建议开启 "Stay awake"; - Chrome 版本门槛:
launchBrowser依赖 Chrome 的命令行文件机制与自定义 remote debugging socket,需要 Chrome 87+ 且开启对应 flag; - 驱动安装开销:默认每次连接都卸载重装两个驱动 APK,CI 环境可评估
omitDriverInstall; - 实验性定位:官方文档明确标注 Android 支持为 experimental,并非所有测试都在真机上跑过,遇到个别方法异常属于已知状态。
排错手段上,device.info(selector) 可以先确认选择器命中了什么控件(返回 AndroidElementInfo);device.shell('logcat -d | tail -n 100') 一类命令可用于查看设备日志;DEBUG=pw:android 可打开驱动安装与 socket 连接的调试日志(源码中的 debug('pw:android') 埋点);截图(设备级 screenshot() 与页面级 page.screenshot())则是视觉回归的直接依据。
仓库中与 Android 自动化相关的入口,便于继续深入:
| 资源 | 路径 |
|---|---|
| AndroidDevice API 参考 | docs/src/mobile-api/class-androiddevice.md |
| Android 总览与示例 | docs/src/mobile-api/class-android.md |
| AndroidInput / AndroidSocket / AndroidWebView 参考 | class-androidinput.md、class-androidsocket.md、class-androidwebview.md |
| 客户端 API 实现 | packages/playwright-core/src/client/android.ts |
| 服务端 ADB/驱动实现 | packages/playwright-core/src/server/android/android.ts |
| 驱动 APK 工程 | packages/playwright-core/src/server/android/driver |
| 协议规范 | packages/protocol/spec/android.yml |
| 集成测试 | tests/android/android.spec.ts、tests/android/device.spec.ts、tests/android/browser.spec.ts、tests/android/launch-server.spec.ts |
至此,AndroidDevice 的全部方法、参数默认值与底层实现路径都已覆盖:从 ADB 连接、驱动 APK 安装、Unix socket 上的 WebView 发现,到 Chrome 命令行注入与 DevTools WebSocket 升级,每一层都可以对照仓库源码验证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00