231553dd095a03ed62fb90802d154561b8bc723e
Go Mobile UIAutomator
Go 语言实现的 Android UIAutomator2 客户端库。纯 Go 实现,零外部依赖,通过 ADB 隧道协议与 Android 设备直连,提供完整的 UI 自动化测试能力。
参考 Python openatx/uiautomator2 项目实现,连接方案与 Python 版本完全一致。
功能特性
- ✅ ADB 隧道 - 纯 Go 实现 ADB TCP 协议,通过 ADB 隧道直连设备,无需端口转发
- ✅ 资源内嵌 - 通过
go:embed将 u2.jar/APK 编译进二进制,go get即用,无需额外文件 - ✅ 设备管理 - 设备发现、连接、信息查询
- ✅ UIAutomator2 服务 - 自动启动和管理 UIAutomator2 服务端
- ✅ UI 元素操作 - 点击、滑动、输入、拖拽、手势等完整 UI 操作
- ✅ 元素定位 - 支持 text、resourceId、className、description 等多种定位方式
- ✅ 便捷选择器 -
d.ByText("登录").Click()Python 风格链式调用 - ✅ 输入法集成 - AdbKeyboard 输入法支持,支持中文输入
- ✅ Watcher 监控 - 自动监控并处理弹窗(权限提示、广告等)
- ✅ 文件传输 - 通过 ADB Sync 协议推送文件
- ✅ 应用管理 - 启动、停止、卸载、清除应用数据
- ✅ 截图功能 - 设备屏幕截图
- ✅ 剪贴板 - 读写设备剪贴板
- ✅ Session 会话 - 应用会话管理,自动检测应用存活状态
- ✅ 配置管理 - 线程安全的设备参数配置
- ✅ 错误体系 - 完整的错误类型定义和自动重试机制
- ✅ JSON-RPC - 完整的 JSON-RPC 2.0 通信层
安装
go get github.com/zhuy1228/go-mobile-uiautomator
快速开始
连接设备
package main
import (
"fmt"
"github.com/zhuy1228/go-mobile-uiautomator/libs"
)
func main() {
// 创建设备连接(自动推送 u2.jar、启动 UIAutomator2 服务)
d, err := libs.NewDevice("设备序列号")
if err != nil {
panic(err)
}
defer d.Close()
// 自定义 ADB 地址
// d, err := libs.NewDevice("设备序列号", "192.168.1.100:5037")
// 获取设备信息
info, err := d.Info()
if err != nil {
panic(err)
}
fmt.Printf("屏幕尺寸: %v x %v\n", info["displayWidth"], info["displayHeight"])
}
设备发现
import (
"fmt"
"time"
"github.com/zhuy1228/go-mobile-uiautomator/adb"
"github.com/zhuy1228/go-mobile-uiautomator/libs"
)
// 列出所有连接的设备
payload, err := adb.ListDevicesRaw(libs.DefaultADBAddr, 15*time.Second)
devices := adb.ParseDevicesPayload(payload)
for _, dev := range devices {
fmt.Printf("设备: %s 状态: %s\n", dev.Serial, dev.State)
}
应用管理
// 启动应用(stop=true 先停止再启动)
d.AppStart("com.example.app", "", true)
// 启动应用并指定 Activity
d.AppStart("com.example.app", ".MainActivity", false)
// 获取当前运行的应用信息
info, _ := d.AppCurrent()
fmt.Printf("当前应用: %s PID: %s\n", info["package"], info["pid"])
// 等待应用启动(超时 20 秒,front=true 等待应用到前台)
pid, err := d.AppWait("com.example.app", 20.0, true)
// 停止应用
d.AppStop("com.example.app")
// 清除应用数据
d.AppClear("com.example.app")
// 卸载应用
d.AppUninstall("com.example.app")
UI 元素查找
// ===== 便捷方法(推荐) =====
// 通过 text 查找并操作
d.ByText("登录").Click()
// 通过 textContains 模糊查找
d.ByTextContains("登").Click()
// 通过 resourceId 查找
d.ByResourceId("com.example:id/btn_login").Click()
// 通过 description 查找
d.ByDescription("搜索").Click()
// 通过 className 查找
d.ByClassName("android.widget.EditText").SetText("Hello")
// 多条件组合查找
el := d.By(libs.P{
"className": "android.widget.TextView",
"text": "确定",
})
el.Click()
// ===== FindElement 方法 =====
el, err := d.FindElement(map[string]interface{}{
"resourceId": "com.example:id/btn_login",
})
if err != nil {
panic(err)
}
el.Click()
// 检查元素是否存在
exists, _ := d.Exists(map[string]interface{}{"text": "登录"})
等待操作
el := d.ByText("登录")
// 等待元素出现(超时 10 秒)
found, _ := el.Wait(true, 10.0)
// 等待元素消失
gone, _ := el.WaitGone(10.0)
// 等待元素出现,不存在则返回错误
err := el.MustWait(10.0)
// 设置全局隐式等待超时
d.ImplicitlyWait(20.0)
点击操作
// 坐标点击
d.Click(500, 800)
// 双击
d.DoubleClick(500, 800, 0)
// 长按坐标(2 秒)
d.LongClick(500, 800, 2.0)
// 元素点击
d.ByText("确定").Click()
// 带偏移的点击(相对元素宽高的比例,0.5 = 中心)
d.ByText("确定").ClickWithOffset(0.5, 0.5)
// 点击元素(如果存在),返回是否成功
clicked := d.ByText("确定").ClickExists(5.0)
// 持续点击直到元素消失(最多重试 5 次,间隔 1 秒)
d.ByText("确定").ClickGone(5, 1.0)
// 元素长按(1.5 秒)
d.ByText("确定").LongClick(1.5)
文本输入
el := d.ByResourceId("com.example:id/input")
// 获取元素文本
text, _ := el.GetText()
// 设置文本(先清空再输入)
el.SetText("Hello World")
// 清除文本
el.ClearText()
// 追加文本
el.SendKeys("additional text")
// 使用 AdbKeyboard 输入法(支持中文)
im := libs.NewInputMethod(d)
im.SendKeys("你好世界")
im.ClearText() // 清除文本
im.SendAction("done") // 模拟输入法动作
滑动操作
// 坐标滑动(steps 控制速度,每步约 5ms)
d.Swipe(500, 1500, 500, 500, 55)
// 按持续时间滑动(0.5 秒)
d.SwipeWithDuration(500, 1500, 500, 500, 0.5)
// 多点连续滑动
d.SwipePoints([][2]int{{500, 1500}, {500, 1000}, {500, 500}}, 0.2)
// 使用 SwipeExt 扩展滑动
se := libs.NewSwipeExt(d)
se.Up(0.8, 55) // 向上滑动 80% 屏幕
se.Down(0.5, 55) // 向下滑动 50% 屏幕
se.Left(0.6, 55) // 向左滑动 60% 屏幕
se.Right(0.4, 55) // 向右滑动 40% 屏幕
// 元素内滑动
d.ByClassName("android.widget.ListView").UiSwipe("up", 55)
d.ByClassName("android.widget.ListView").UiSwipe("down", 55)
// 拖拽
d.Drag(100, 200, 500, 600, 0.5)
d.ByText("拖拽我").DragTo(500, 600, 0.5)
滚动查找
list := d.ByClassName("android.widget.ListView")
// 向前/向后滚动
list.ScrollForward(true, 55) // 垂直向前
list.ScrollBackward(true, 55) // 垂直向后
// 滚动到顶部/底部
list.ScrollToBeginning(true, 10, 55) // 最多 10 次
list.ScrollToEnd(true, 10, 55)
// 滚动直到目标元素可见
list.ScrollTo(map[string]interface{}{"text": "目标文本"}, true)
// 快速滑动(Fling)
list.FlingForward(true)
list.FlingBackward(true)
list.FlingToBeginning(true, 10)
list.FlingToEnd(true, 10)
手势操作
el := d.ByResourceId("com.example:id/image")
// 捏合(缩小)
el.PinchIn(50, 10)
// 捏合(放大)
el.PinchOut(50, 10)
子元素和兄弟元素
// 查找子元素
parent := d.ByClassName("android.widget.LinearLayout")
child, _ := parent.Child(map[string]interface{}{"className": "android.widget.Button"})
child.Click()
// 查找兄弟元素
sibling, _ := d.ByText("标题").Sibling(map[string]interface{}{"className": "android.widget.Button"})
sibling.Click()
// 通过文本在子元素中搜索
item, _ := parent.ChildByText("目标文本", map[string]interface{}{"className": "android.widget.TextView"})
// 获取匹配元素数量
count, _ := d.ByClassName("android.widget.Button").Count()
// 获取指定索引的元素
d.ByClassName("android.widget.Button").Instance(0).Click() // 第一个
d.ByClassName("android.widget.Button").Instance(-1).Click() // 最后一个
Watcher 弹窗监控
// 创建 Watcher 上下文(builtin=true 自动添加常见弹窗规则)
w := libs.NewWatchContext(d, true)
// 添加监控规则:点击"同意"按钮
w.WhenText("同意").Click()
// 添加监控规则:点击"允许"按钮
w.WhenText("ALLOW").Click()
// 添加监控规则:按下返回键关闭弹窗
w.WhenText("取消").Press("back")
// 添加监控规则:自定义回调
w.WhenDescription("广告").Call(func(d *libs.Device) error {
fmt.Println("检测到广告弹窗")
return d.Press("back")
})
// 启动监控(后台运行)
w.Start()
// 等待页面稳定(5 秒内无弹窗,超时 30 秒)
w.WaitStable(5.0, 30.0)
// 停止监控
w.Stop()
Session 会话管理
// 创建应用会话(attach=false 启动新实例)
session, err := libs.NewSession(d, "com.example.app", false)
if err != nil {
panic(err)
}
// 检查应用是否仍在运行
if session.Running() {
fmt.Println("应用运行中, PID:", session.PID())
}
// 会话中的 JSON-RPC 调用会自动检查应用状态
// 如果应用退出,会返回 SessionBrokenError
// 重启应用
session.Restart()
// 关闭会话
session.Close()
截图
// 获取截图(JPEG 格式字节数据)
imgData, err := d.Screenshot()
if err != nil {
panic(err)
}
// 保存到文件
os.WriteFile("screenshot.png", imgData, 0644)
设备控制
// 亮屏/息屏
d.ScreenOn()
d.ScreenOff()
// 解锁屏幕
d.Unlock()
// 按键操作
d.Press("home")
d.Press("back")
d.Press("recent")
d.Press("volume_up")
d.Press("volume_down")
d.Press("power")
d.Press("enter")
// 发送按键码
d.PressKeyCode(4, 0) // KEYCODE_BACK
// 长按按键
d.LongPress("power")
// 发送按键事件
d.Keyevent("KEYCODE_MENU")
// 屏幕方向
orientation, _ := d.Orientation() // 获取当前方向
d.SetOrientation("natural") // 竖屏
d.SetOrientation("left") // 左横屏
d.FreezeRotation(true) // 冻结旋转
// 打开通知栏/快速设置
d.OpenNotification()
d.OpenQuickSettings()
// 打开 URL
d.OpenURL("https://www.example.com")
// 剪贴板
d.SetClipboard("复制的文本")
text, _ := d.GetClipboard()
// Toast
d.MakeToast("提示消息", 2000) // 2000 毫秒
toast, _ := d.GetLastToast()
d.ClearToast()
// 获取页面层级结构
hierarchy, _ := d.DumpHierarchy(false, 50)
// 执行 Shell 命令
result, _ := d.Shell("ls", "/sdcard/")
fmt.Println(result.Output)
// 获取窗口大小
w, h, _ := d.WindowSize()
fmt.Printf("窗口大小: %d x %d\n", w, h)
// 获取设备 WLAN IP
ip, _ := d.WlanIP()
// 推送文件到设备
d.Push("local/file.txt", "/sdcard/file.txt")
配置管理
// 设置等待超时(秒)
d.Settings().Set("wait_timeout", 30.0)
// 获取配置
timeout := d.Settings().GetFloat64("wait_timeout")
// 设置操作延迟
d.Settings().Set("operation_delay", []float64{0.5, 1.0}) // [操作前延迟, 操作后延迟]
// 设置需要延迟的操作方法
d.Settings().Set("operation_delay_methods", []string{"click", "swipe"})
// 设置隐式等待超时
d.ImplicitlyWait(20.0)
UI 选择器
// 创建选择器
selector := libs.MustNew(map[string]interface{}{
"className": "android.widget.TextView",
"text": "登录",
})
// 添加子元素选择
selector.Child(map[string]interface{}{
"resourceId": "com.example:id/button",
"instance": 0,
})
// 序列化为 JSON
jsonData, _ := selector.ToJSON()
UIAutomator2 服务部署
import "github.com/zhuy1228/go-mobile-uiautomator/services"
// 安装 UIAutomator2 JAR(自动从内嵌资源推送,检查设备端是否已存在)
err := services.InstallServiceJar(addr, serial, false)
// 安装 UIAutomator2 APK(自动从内嵌资源推送)
err = services.InstallServiceApk(addr, serial, false)
// 强制重新安装(force=true 跳过存在检查)
err = services.InstallServiceJar(addr, serial, true)
资源文件通过
go:embed编译进二进制,无需在本地维护 JAR/APK 文件。
项目结构
go-mobile-uiautomator/
├── adb/ # ADB 协议实现
│ ├── connect.go # 连接管理、ADB 隧道、命令发送/接收
│ ├── device.go # 设备发现、APK 安装
│ ├── sync.go # 文件同步协议(Sync Push)
│ └── doc.go # 包文档
├── libs/ # UIAutomator2 客户端核心库
│ ├── device.go # 核心设备客户端(连接、操作、应用管理)
│ ├── uiobject.go # UI 元素操作(点击、输入、滑动、滚动)
│ ├── jsonrpc.go # JSON-RPC 2.0 通信层
│ ├── selector.go # UI 选择器(定位条件构建)
│ ├── request.go # ADB 隧道 + 自定义 HTTP Transport
│ ├── input.go # AdbKeyboard 输入法集成
│ ├── watcher.go # Watcher 弹窗自动监控
│ ├── session.go # 应用会话管理
│ ├── swipe_ext.go # 扩展滑动操作
│ ├── settings.go # 线程安全配置管理
│ ├── errors.go # 完整错误类型定义
│ ├── proto.go # 协议常量和枚举
│ └── doc.go # 包文档
├── services/ # 服务模块
│ ├── install_service.go # UIAutomator2 安装服务
│ └── doc.go # 包文档
├── assets/ # 资源文件(通过 go:embed 编译进二进制)
│ ├── embed.go # go:embed 指令,导出 JarData/ApkData/ApkTestData
│ ├── u2.jar # UIAutomator2 服务端 JAR
│ ├── app-uiautomator.apk # UIAutomator2 APK
│ ├── app-uiautomator-test.apk # UIAutomator2 测试 APK
│ └── sync.sh # 资源同步脚本
├── cmd/ # 主程序入口
│ └── main.go
├── go.mod # Go 模块定义(零外部依赖)
└── README.md
架构说明
┌─────────────────────────────────────────────────┐
│ 用户代码 │
├─────────────────────────────────────────────────┤
│ libs.Device / libs.UiObject / libs.Session │
│ (设备操作 / UI 元素 / 会话管理) │
├─────────────────────────────────────────────────┤
│ libs.JsonRpcWrapper │
│ (JSON-RPC 2.0 通信层) │
├─────────────────────────────────────────────────┤
│ libs.AdbTunnelDevice + http.Transport │
│ (ADB 隧道 + 标准 HTTP 客户端) │
├─────────────────────────────────────────────────┤
│ adb.CreateTunnel (纯 Go ADB 协议) │
│ (TCP → ADB Server → transport → tcp:9008) │
├─────────────────────────────────────────────────┤
│ assets.JarData / assets.ApkData (go:embed) │
│ (资源文件编译进二进制,零配置部署) │
├─────────────────────────────────────────────────┤
│ Android 设备 │
│ UIAutomator2 HTTP 服务 (端口 9008) │
└─────────────────────────────────────────────────┘
连接原理
与 Python uiautomator2 采用完全相同的 ADB 隧道方案:
- 每次 HTTP 请求通过
adb.CreateTunnel()建立一条 ADB 隧道到设备端口 9008 http.Transport的DialContext被替换为 ADB 隧道连接- Go 标准
http.Client在隧道上发送完整的 HTTP/1.1 请求 - 请求完成后隧道自动关闭,无需
adb forward,不占用本地端口
Go http.Client → http.Transport.DialContext → adb.CreateTunnel()
↓ ↓
HTTP/1.1 请求 ←→ ADB Server ←→ 设备:9008 (UIAutomator2)
ADB 协议实现
本项目实现了以下 ADB 协议功能:
| 功能 | 命令 | 说明 |
|---|---|---|
| 设备列表 | host:devices-l |
列出所有连接的设备 |
| 设备路由 | host:transport:<serial> |
切换到指定设备 |
| Shell 执行 | shell:<command> |
执行 Shell 命令 |
| ADB 隧道 | tcp:<port> |
建立到设备端口的直连隧道 |
| 文件同步 | sync: |
启动文件传输协议 |
| 文件推送 | SEND/DATA/DONE |
Sync 协议传输文件 |
错误类型
本库定义了完整的错误类型层次,便于精确错误处理:
| 错误类型 | 说明 |
|---|---|
DeviceError |
设备通用错误 |
ConnectError |
连接错误 |
HTTPError |
HTTP 请求错误 |
HTTPTimeoutError |
HTTP 超时错误 |
AdbShellError |
ADB Shell 执行错误 |
RPCError |
JSON-RPC 调用错误 |
RPCUnknownError |
未知 RPC 错误 |
RPCInvalidError |
无效 RPC 响应 |
RPCStackOverflowError |
Java 端栈溢出 |
UiObjectNotFoundError |
UI 元素未找到 |
UiAutomationNotConnectedError |
UIAutomation 服务断开 |
HierarchyEmptyError |
UI 层级为空 |
LaunchUiAutomationError |
UIAutomator2 启动失败 |
SessionBrokenError |
应用会话中断 |
AppNotFoundError |
应用未安装 |
InputIMEError |
输入法错误 |
常见问题
Q: 如何获取设备序列号?
运行 adb devices 或使用代码:
payload, _ := adb.ListDevicesRaw(libs.DefaultADBAddr, 15*time.Second)
devices := adb.ParseDevicesPayload(payload)
for _, dev := range devices {
fmt.Println("Serial:", dev.Serial)
}
Q: UIAutomator2 服务启动失败?
确保:
- 设备已正确连接且已授权 USB 调试
- ADB Server 正在运行(默认
127.0.0.1:5037) - 使用
NewDevice()会自动从内嵌资源推送 JAR/APK 并启动服务,无需手动管理
u2.jar 和 APK 通过
go:embed编译进二进制,go get后即可直接使用,无需额外下载资源文件。
Q: 如何处理弹窗干扰自动化?
使用 Watcher 机制自动处理弹窗:
w := libs.NewWatchContext(d, true) // builtin=true 自带常见弹窗规则
w.WhenText("允许").Click()
w.Start()
defer w.Stop()
Q: 如何输入中文?
使用 AdbKeyboard 输入法:
im := libs.NewInputMethod(d)
im.SendKeys("你好世界")
Q: 连接已有服务(不启动新服务)?
// 适用于 UIAutomator2 已经在设备上运行的场景
d := libs.NewDeviceWithoutStart("emulator-5554")
d.ByText("Hello").Click()
开发计划
- 纯 Go ADB 协议实现
- go:embed 资源内嵌(零配置部署)
- ADB 隧道直连(与 Python 一致)
- 设备发现和连接管理
- JSON-RPC 2.0 通信层
- 完整 UI 元素操作(点击、滑动、输入、拖拽)
- 便捷选择器(ByText、ByResourceId 等)
- Watcher 弹窗自动监控
- Session 会话管理
- AdbKeyboard 输入法集成
- 截图功能
- 应用生命周期管理
- 完整错误类型体系
- 线程安全配置管理
- XPath 选择器支持
- 设备录屏功能
- 批量设备管理
- 完整单元测试
依赖
零外部依赖 — 仅使用 Go 标准库。
module github.com/zhuy1228/go-mobile-uiautomator
go 1.25.1
许可证
MIT License
致谢
本项目参考了以下开源项目:
- openatx/uiautomator2 — Python Android 自动化框架
- Android ADB Protocol — ADB 协议规范
Description
Languages
Go
99.2%
Shell
0.6%
Python
0.2%