首页
/ Playwright Android 自动化完全指南:AndroidDevice API 详解与 ADB 驱动实现原理

Playwright Android 自动化完全指南:AndroidDevice API 详解与 ADB 驱动实现原理

2026-09-06 13:29:26作者:沈韬淼Beryl

Playwright 自 v1.9 起提供了实验性的 Android 自动化支持,其核心抽象是 AndroidDevice 类:它代表一台通过 ADB 连接的真实设备或模拟器(AVD),既能对原生 UI 控件做点击、滑动、填写等交互,也能接管设备上的 Chrome 浏览器与 WebView,让同一套 Page / BrowserContext API 直接运行在移动端。读完本篇,你将掌握 AndroidDevice 的全部方法签名与参数默认值、从 ADB 连接到浏览器接管的完整实战流程,以及从源码层面看懂 Playwright 如何在设备上安装驱动 APK、通过本地抽象 socket 与 WebView 建立 DevTools 通道的底层机制。

一、AndroidDevice 是什么:概念、前提与获取方式

根据官方 API 文档 class-androiddevice.mdAndroidDevice 表示一台已连接的设备(真机或模拟),可通过 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)、textdesc(content description)、pkgclazzcheckable / 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? } 等待匹配 pkgsocketName 的 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 | Bufferopts: { 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 可选,默认 644rw-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 参数列表),以及 proxyargs(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 可以看到完整链路:

  1. am force-stop <pkg> 先杀掉目标浏览器(默认 com.android.chrome);
  2. 生成一个唯一的 socket 名 playwright_<guid>_devtools_remote(测试模式下为固定名 webview_devtools_remote_playwright_test);
  3. 组装 Chrome 启动参数:--disable-fre--no-default-browser-check--remote-debugging-socket-name=<socketName>、Android 专用 Chromium 开关,以及用户传入的 proxy(会翻译成 --proxy-server / --proxy-bypass-list)与 args
  4. 命令行的特殊字符容易在 shell 中出问题,所以源码把它 base64 编码后写入设备上的 /data/local/tmp/chrome-command-line,再用 am start -a android.intent.action.VIEW -d about:blank <pkg> 拉起浏览器(Chrome 的 command-line 文件机制会读取该文件);
  5. 通过 open('localabstract:<socketName>') 打开这个 DevTools 抽象 socket,手工完成一次 HTTP Upgrade 握手,把 socket 包装成 AndroidBrowser(内置 WebSocket 收发器),随后以 persistent 持久化上下文模式连接 CRBrowser,返回默认 BrowserContext
  6. 成功后删除临时命令行文件;失败则关闭已建立的上下文并抛错。

这也解释了为什么文档要求开启 "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(默认不可猜测的随机串)和 omitDriverInstallconnect(endpoint, options?) 侧则支持 headersslowMo(毫秒级减速,便于观察)与 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 限制向下搜索深度;
  • 布尔字段(clickableenabledselected 等)与 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 读型号。AndroidDeviceshell / 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)会:

  1. am force-stop com.microsoft.playwright.androiddriver 停掉旧驱动;
  2. 若未设置 omitDriverInstall,先 cmd package uninstall 两个驱动包(androiddriverandroiddriver.test),再从 Playwright 安装目录读取 android-driver.apkandroid-driver-target.apk,用与 installApk 相同的 socket 通道装上去(文件缺失时提示执行 playwright install android);
  3. am instrument -w com.microsoft.playwright.androiddriver.test/androidx.test.runner.AndroidJUnitRunner 启动 instrument 进程;
  4. 轮询 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):

  1. 每 500ms 执行 shell:cat /proc/net/unix | grep webview_devtools_remote,扫描系统 Unix socket 表;
  2. 用正则提取 webview_devtools_remote_<pid>[<name>] 形式的 socket 名,并从 socket 名中解析出宿主进程 PID,再用 ps -A | grep <pid> 反查出包名;
  3. 与本地缓存比对:新出现的 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.mdclass-androidsocket.mdclass-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.tstests/android/device.spec.tstests/android/browser.spec.tstests/android/launch-server.spec.ts

至此,AndroidDevice 的全部方法、参数默认值与底层实现路径都已覆盖:从 ADB 连接、驱动 APK 安装、Unix socket 上的 WebView 发现,到 Chrome 命令行注入与 DevTools WebSocket 升级,每一层都可以对照仓库源码验证。

登录后查看全文
热门项目推荐
相关项目推荐