Python uiautomator2安卓自动化实战:从环境搭建到项目部署
1. 项目概述:从手动到自动的安卓测试革命
如果你是一名安卓开发者、测试工程师,或者只是对自动化感兴趣的技术爱好者,那么你一定对在手机屏幕上重复点击、滑动、输入的操作感到厌倦。尤其是在进行回归测试、数据采集或者批量操作时,手动执行不仅效率低下,而且极易出错,让人身心俱疲。这正是“告别手动操作”这个标题直击的痛点。今天,我想分享的正是我过去几年在安卓自动化领域,从零开始摸索,最终稳定落地的一套基于 Python 和 uiautomator2 的解决方案。这套方案的核心价值在于,它绕开了官方 Instrumentation 的复杂性和 Appium 的庞大与不稳定,直接与安卓系统的原生 UI 自动化框架对话,实现了轻量、高效且稳定的控制。
uiautomator2 是一个 Python 第三方库,它本质上是一个 RPC(远程过程调用)服务,通过在手机上安装一个守护进程(atx-agent),将安卓系统自带的 UiAutomator 框架的能力暴露出来,允许我们在电脑上通过 Python 代码发送指令来控制手机。这比基于 WebDriver 协议的方案(如 Appium)更底层,速度更快,也更稳定。而 weditor 则是 uiautomator2 生态中一个至关重要的可视化辅助工具,它允许我们像使用浏览器开发者工具一样,实时查看手机界面的控件层级和属性,极大地简化了元素定位的难度。然而,正如标题中提到的“避坑指南”,weditor 的安装和使用过程并非一帆风顺,其中有不少细节和兼容性问题,我会在后续详细拆解。
这个项目适合所有希望将重复性安卓操作自动化的人。无论你是想自动刷短视频、定时签到、批量处理消息,还是进行专业的 UI 自动化测试,这套组合拳都能为你提供一个坚实可靠的起点。接下来,我将从环境搭建、核心原理、实战编码到避坑排错,为你完整呈现如何构建一个属于自己的安卓自动化工作流。
2. 环境搭建与工具链深度解析
工欲善其事,必先利其器。一个稳定、隔离的 Python 环境是这一切的基础。我强烈建议使用 Anaconda 或 Miniconda 来创建独立的虚拟环境,这能避免不同项目间的包版本冲突。对于新手,我推荐 Miniconda,它更轻量。
2.1 Python 环境与核心库安装
首先,确保你的电脑上已经安装了 Python(3.7及以上版本)。打开命令行(Windows 的 CMD/PowerShell,macOS/Linux 的 Terminal),创建一个新的虚拟环境并激活它。
# 创建名为 `android_auto` 的虚拟环境,指定 Python 版本
conda create -n android_auto python=3.9
# 激活环境
conda activate android_auto
接下来,安装核心库 uiautomator2 。这里有一个关键点:直接使用 pip 安装最新版有时会遇到依赖问题。我个人的经验是,指定一个经过广泛验证的稳定版本。
pip install uiautomator2==2.16.22
这个命令不仅会安装 uiautomator2 库本身,还会在后续步骤中引导我们安装手机端服务。同时,为了编写和调试脚本,我们还需要安装用于图像识别的 pillow 库和用于控制等待的 retry 库。
pip install pillow retry
2.2 安卓设备连接与初始化
确保你的安卓手机已开启“开发者选项”和“USB调试”模式。不同手机开启方式略有不同,通常在“设置”-“关于手机”中连续点击“版本号”7次即可激活开发者选项。然后用 USB 数据线连接电脑。
在命令行中,使用 adb devices 命令检查设备是否被识别。如果没有安装 ADB,需要先下载 Android SDK Platform-Tools 并配置环境变量。连接成功后,你会看到设备序列号。
现在,进行 uiautomator2 的初始化。这一步会在手机上安装必要的守护进程(atx-agent)和测试用的 APK。
python -m uiautomator2 init
注意 :
init命令可能会因为网络问题(需要从 GitHub 下载资源)而失败。如果失败,可以尝试使用--mirror参数指定国内镜像,例如python -m uiautomator2 init --mirror https://mirrors.aliyun.com/pypi/simple/。初始化成功后,手机上会出现一个名为 “ATX” 的应用。
2.3 Weditor 的安装与“避坑”实战
Weditor 是定位元素的利器,但其安装过程可能是第一个“坑”。官方推荐通过 uiautomator2 命令行安装:
python -m weditor
第一次运行会自动安装并尝试在浏览器中打开界面。但这里常见的问题有:
- 端口冲突 :默认使用 17310 端口。如果该端口被占用,启动会失败。可以通过
--port参数指定其他端口,如python -m weditor --port 8090。 - 浏览器无法自动打开 :这没关系,命令行会输出一个本地 URL(如
http://localhost:17310),手动在浏览器中打开即可。 - 连接设备失败 :确保
adb devices中有且仅有一台设备在线。Weditor 依赖于 ADB 连接。
如果通过上述命令安装失败(例如出现编码错误或依赖缺失),可以尝试直接用 pip 安装 weditor,然后单独运行:
pip install weditor
weditor # 或 python -m weditor
在浏览器中打开 Weditor 后,你需要点击界面上的“刷新”按钮来连接手机。连接成功后,当前手机屏幕的截图和完整的 UI 控件层级树(类似于 HTML 的 DOM 树)就会显示出来。你可以点击图中的元素,右侧会自动显示该元素的所有属性,如 resource-id , text , class , bounds 等,这些属性就是我们编写自动化脚本时用于定位元素的“坐标”。
实操心得 :Weditor 在解析某些复杂混合应用(如大量使用 Flutter 或游戏 Canvas)的界面时,可能无法获取所有控件信息。此时,需要结合
bounds(坐标)定位或者图像识别等备用方案。另外,保持 Weditor 和 uiautomator2 库版本的匹配也很重要,不匹配可能导致连接不稳定。
3. uiautomator2 核心API与自动化逻辑构建
环境就绪后,我们来深入核心,看看如何用代码驱动手机。首先,在 Python 脚本中导入库并连接设备。
import uiautomator2 as u2
# 连接设备方式一:通过设备序列号(adb devices 获取)
d = u2.connect(‘你的设备序列号’)
# 连接设备方式二:通过无线网络(需先用USB连接一次并执行`adb tcpip 5555`)
# d = u2.connect(‘192.168.1.100:5555’)
# 连接设备方式三:自动连接当前唯一设备(最常用)
d = u2.connect()
3.1 元素定位:自动化脚本的基石
定位元素是自动化操作的第一步,也是最关键的一步。uiautomator2 提供了多种定位方式,其语法与 Android 原生 UiAutomator 高度一致。
1. 通过 Resource ID 定位: 这是最优先推荐的方式,精准且稳定。前提是应用元素有唯一的 resource-id 。
d(resourceId=“com.example.app:id/login_button”).click()
2. 通过文本内容定位: 适用于按钮、标签等有明确文字的元素。
d(text=“登录”).click()
d(textContains=“登录”).click() # 包含“登录”二字
3. 通过类名定位: 可以定位某一类控件,如所有 TextView 。
d(className=“android.widget.TextView”)
4. 通过描述(Content Description)定位: 适用于无障碍访问或图像按钮。
d(description=“搜索按钮”).click()
5. 组合定位: 当单一属性不唯一时,可以组合使用。
d(className=“android.widget.Button”, text=“确定”).click()
6. 通过坐标定位(应作为最后手段): 当元素无法通过属性定位时(如游戏内的图形),可以使用绝对坐标。但这种方式在不同分辨率设备上不兼容,应尽量避免。
d.click(x, y) # 点击屏幕坐标(x, y)
7. 相对定位与兄弟节点定位: uiautomator2 支持类似 XPath 的链式调用,但更推荐使用 child , sibling 等方法。
# 定位一个元素,然后找它的子元素或兄弟元素
parent = d(className=“android.widget.LinearLayout”)
child_button = parent.child(className=“android.widget.Button”)
在 Weditor 中,你可以轻松地通过点击界面获取这些定位信息。选中元素后,Weditor 会生成对应的 Python 代码片段,直接复制使用即可,这大大提升了开发效率。
3.2 常用操作API:模拟真实用户行为
定位到元素后,就可以对其执行操作了。以下是最常用的操作:
- 点击 :
.click() - 长按 :
.long_click() - 输入文本 :
.set_text(“Hello World”)。注意,输入前最好先.click()一下确保焦点在输入框。 - 清除文本 :
.clear_text() - 滑动 :
d.swipe(sx, sy, ex, ey, duration)从点 (sx, sy) 滑动到 (ex, ey),duration 是持续时间(秒)。 - 拖动 :
d.drag(sx, sy, ex, ey, duration)与滑动类似,但用于可拖动元素。 - 按键事件 :
d.press(“home”),d.press(“back”),d.press(“volume_up”)等。
一个完整的操作示例:自动打开微信并搜索。
d.app_start(“com.tencent.mm”) # 启动微信
d(text=“搜索”).click() # 点击顶部搜索框
d(className=“android.widget.EditText”).set_text(“公众号名称”) # 输入文本
d.press(“enter”) # 模拟按下回车键
3.3 等待与断言:让脚本更健壮
在自动化中,等待是必不可少的,因为网络加载、页面渲染都需要时间。uiautomator2 提供了智能等待。
- 隐式等待(全局设置) :
d.implicitly_wait(10.0)设置查找元素时的最大等待时间(秒)。 - 显式等待(针对特定元素) :
# 等待“登录成功”的提示出现,最多等10秒
d(text=“登录成功”).wait(timeout=10.0)
# 等待元素消失
d(text=“加载中…”).wait_gone(timeout=15.0)
断言用于验证操作结果,是自动化测试的核心。
# 检查元素是否存在
if d(text=“欢迎回来”).exists:
print(“登录成功”)
else:
print(“登录失败”)
# 可以在这里截图保存现场
d.screenshot(“login_failed.png”)
4. 实战:构建一个完整的自动化任务流
让我们通过一个更复杂的例子,串联起所有知识点:自动完成某个新闻APP的每日签到、阅读任务,并截图保存。
import uiautomator2 as u2
import time
from datetime import datetime
def daily_task(device_serial=None):
"""
执行新闻APP的每日任务
"""
# 1. 连接设备
d = u2.connect(device_serial) if device_serial else u2.connect()
print(f“已连接设备: {d.info}”)
# 2. 启动目标APP
app_package = “com.example.newsapp”
print(“正在启动应用...”)
d.app_start(app_package)
time.sleep(3) # 等待应用冷启动
# 3. 处理可能的弹窗(如青少年模式)
if d(text=“我知道了”).exists:
d(text=“我知道了”).click()
time.sleep(1)
# 4. 执行签到
print(“开始执行签到...”)
# 方法一:通过签到按钮的固定特征定位
sign_btn = d(resourceId=“com.example.newsapp:id/tv_sign”)
if sign_btn.exists and sign_btn.info[‘enabled’]: # 检查是否存在且可点击
sign_btn.click()
print(“签到成功!”)
d.toast.show(“签到成功”, 1.0) # 手机端显示一个短暂的Toast(需要atx-agent支持)
else:
print(“今日已签到或未找到签到按钮。”)
time.sleep(2)
# 5. 执行阅读任务(例如,阅读3篇文章)
print(“开始执行阅读任务...”)
articles_to_read = 3
for i in range(articles_to_read):
print(f“正在阅读第 {i+1} 篇文章...”)
# 假设首页是一个文章列表,点击第一条
if d(className=“android.widget.ListView”).exists:
first_article = d(className=“android.widget.ListView”).child(className=“android.widget.RelativeLayout”)
if first_article.exists:
first_article.click()
time.sleep(5) # 模拟阅读时间
d.press(“back”) # 返回列表页
time.sleep(2)
else:
print(“未找到文章列表,可能页面加载失败。”)
break
else:
print(“不在文章列表页面,尝试返回主页...”)
d.press(“back”)
time.sleep(2)
# 6. 任务完成,截图并保存
timestamp = datetime.now().strftime(“%Y%m%d_%H%M%S”)
screenshot_path = f“daily_task_{timestamp}.png”
d.screenshot(screenshot_path)
print(f“任务完成!截图已保存至: {screenshot_path}”)
# 7. 返回桌面,清理后台(可选)
d.press(“home”)
# d.app_stop(app_package) # 强制停止应用
if __name__ == “__main__”:
daily_task()
这个脚本展示了从启动、交互、逻辑判断到结果保存的完整流程。其中, time.sleep() 的使用是简单的固定等待,在实际项目中,应尽可能替换为前面提到的智能等待( wait() ),以提高脚本的效率和稳定性。
5. 高级技巧与性能优化
当脚本越来越复杂时,你需要考虑代码结构、可维护性和执行效率。
5.1 Page Object 设计模式
这是UI自动化测试中经典的设计模式,将每个页面封装成一个类,页面的元素定位和操作作为类的方法。这能极大提高代码的可读性和可维护性。
# login_page.py
class LoginPage:
def __init__(self, d):
self.d = d
@property
def username_input(self):
return self.d(resourceId=“com.example.app:id/et_username”)
@property
def password_input(self):
return self.d(resourceId=“com.example.app:id/et_password”)
@property
def login_button(self):
return self.d(resourceId=“com.example.app:id/btn_login”)
def login(self, username, password):
self.username_input.set_text(username)
self.password_input.set_text(password)
self.login_button.click()
return HomePage(self.d) # 返回下一个页面对象
# 在主脚本中使用
from login_page import LoginPage
login_page = LoginPage(d)
home_page = login_page.login(“your_username”, “your_password”)
5.2 图像识别辅助定位
对于游戏或部分无法获取控件信息的原生组件,可以结合图像识别。使用 pillow 库进行截图和比对。
from PIL import Image
import io
def find_image_and_click(template_path, confidence=0.8):
"""
在屏幕上查找模板图片并点击其中心
:param template_path: 模板小图片的路径
:param confidence: 匹配置信度,0-1之间
"""
# 1. 截取当前屏幕
screen_byte = d.screenshot()
screen_img = Image.open(io.BytesIO(screen_byte))
# 2. 加载模板图片
template_img = Image.open(template_path)
# 3. 使用简单的RGB像素比对(实际项目中推荐使用opencv的matchTemplate)
# 此处为简化示例,真实场景需实现图像匹配算法
# ...
# 假设找到了位置 (x, y)
# d.click(x, y)
注意 :纯图像识别计算量大、受分辨率/亮度影响,且不易维护,应仅作为属性定位失效时的补充手段。
5.3 并行执行与多设备管理
如果你有多台测试设备,uiautomator2 可以轻松实现并行自动化。
import threading
device_serials = [“serial1”, “serial2”, “192.168.1.100:5555”]
def task_for_device(serial):
d = u2.connect(serial)
# ... 执行具体的自动化任务
print(f“Device {serial} task finished.”)
threads = []
for serial in device_serials:
t = threading.Thread(target=task_for_device, args=(serial,))
t.start()
threads.append(t)
for t in threads:
t.join()
6. Weditor 深度避坑与疑难杂症排查
尽管 weditor 非常好用,但在实际使用中,我踩过的坑不计其数。这里集中梳理一下最常见的几个问题及其解决方案。
6.1 连接失败与“初始化”问题
问题现象 :Weditor 界面一直显示“正在连接…”或“设备未初始化”。
- 排查步骤1 :检查
adb devices命令,确认设备已连接且状态为device,而不是unauthorized(未授权)。如果是未授权,需要在手机弹出的“允许USB调试”对话框中点击确认。 - 排查步骤2 :确认 uiautomator2 服务已初始化。在电脑命令行执行
python -m uiautomator2 init确保成功。可以手动检查手机上是否安装了 “ATX” 应用。 - 排查步骤3 :重启 atx-agent。在命令行执行
adb shell /data/local/tmp/atx-agent server --stop然后adb shell /data/local/tmp/atx-agent server --start。 - 排查步骤4 :更换 Weditor 连接端口。有时端口被占用或出现奇怪问题,用
python -m weditor --port 8090换一个端口试试。
6.2 控件信息抓取不全或为空
问题现象 :Weditor 能连接并截图,但控件树是空的,或者只有部分层级。
- 原因与解决1 : 应用使用非原生控件或游戏引擎 。如 Flutter、Unity、Cocos 等。uiautomator2 只能抓取标准安卓控件。此时需采用坐标定位或图像识别。
- 原因与解决2 : 屏幕处于锁屏或非目标应用界面 。确保手机已解锁,并且停留在你想要分析的应用界面上。
- 原因与解决3 : Weditor 版本与 uiautomator2 库版本不兼容 。尝试将 weditor 升级到最新版或降至与 uiautomator2 匹配的版本。可以尝试
pip install -U weditor。 - 原因与解决4 : 手机系统权限问题 。某些手机(如小米、华为)需要对“ATX”应用授予“显示在其他应用上层”或“无障碍服务”权限。请在手机设置中检查并授权。
6.3 元素定位符不稳定,运行时找不到
问题现象 :在 Weditor 里能看到元素,也能生成代码,但运行脚本时却报错 UiObjectNotFoundError 。
- 策略1:使用更稳定的定位属性 。优先选择
resource-id,它是开发人员赋予的唯一标识,最稳定。其次是text和content-desc。避免单独使用className,因为它通常不唯一。 - 策略2:增加智能等待,减少硬编码等待 。用
d(text=“xx”).wait(timeout=10)代替time.sleep(10)。 - 策略3:采用相对定位或兄弟定位 。有时元素的绝对路径会变,但它在父容器中的相对位置不变。使用
child,sibling等方法可以提高鲁棒性。 - 策略4:利用异常处理进行重试 。
from retry import retry
@retry(tries=3, delay=1)
def click_safe(element):
if element.exists:
element.click()
else:
raise Exception(“Element not found”)
click_safe(d(text=“不稳定的按钮”))
6.4 性能问题与脚本稳定性提升
- 减少不必要的截图 :
d.screenshot()和 Weditor 的持续刷新都会产生大量 ADB 数据传输,影响速度。在稳定运行的脚本中,仅在出错时截图。 - 合理使用等待 :滥用
time.sleep()会极大拖慢脚本。多用wait()和wait_gone()这种条件等待。 - 元素操作前判断状态 :在
click()前,可以判断元素是否enabled和exists,避免无效操作。 - 定期重启设备与服务 :长时间运行大量自动化任务后,手机内存和 atx-agent 服务可能会不稳定。定期重启手机或重启 atx-agent 服务能解决很多玄学问题。
7. 项目封装、部署与持续集成思路
当你的自动化脚本成熟后,可以考虑将其工程化,方便团队协作和持续运行。
1. 项目目录结构:
android_auto_project/
├── config/ # 配置文件
│ ├── devices.yaml # 设备配置
│ └── settings.yaml # 全局设置
├── pages/ # Page Object 页面类
│ ├── __init__.py
│ ├── login_page.py
│ └── home_page.py
├── testcases/ # 测试用例
│ ├── test_daily_task.py
│ └── test_login.py
├── utils/ # 工具函数
│ ├── image_helper.py
│ └── logger.py
├── reports/ # 测试报告和截图
├── requirements.txt # 依赖列表
└── run.py # 主运行入口
2. 使用配置文件管理设备信息:
# config/devices.yaml
devices:
- serial: “手机1序列号”
nickname: “小米测试机”
platform_version: “12”
- serial: “192.168.1.101:5555”
nickname: “华为无线机”
platform_version: “10”
3. 集成到 CI/CD(如 Jenkins): 可以编写一个 Shell 脚本,在 Jenkins 任务中执行以下步骤:
- 连接并解锁测试手机。
- 安装或更新待测应用。
- 运行你的 Python 自动化测试套件(例如使用
pytest)。 - 收集生成的日志、报告和截图。
- 将结果邮件通知团队。
4. 日志与报告: 使用 Python 标准的 logging 模块记录运行日志。对于测试报告,可以集成 pytest-html 或 Allure 来生成美观的 HTML 报告,清晰展示每个步骤的成功与否以及出错时的现场截图。
从手动点击到自动化脚本,不仅仅是效率的提升,更是工作模式的变革。它让你从重复劳动中解放出来,去关注更重要的逻辑验证和异常场景。uiautomator2 与 weditor 的组合,以其 Python 的简洁语法和直接高效的底层控制,为安卓自动化提供了一个极具吸引力的选择。虽然过程中会遇到 weditor 连接、元素定位稳定性等挑战,但一旦掌握了这些问题的应对方法,构建稳定可靠的自动化任务就变成了一件充满成就感的事情。记住,好的自动化脚本不是一蹴而就的,它需要你在实际运行中不断观察、调试和优化。开始动手写你的第一个脚本吧,从自动给某个应用签到开始,你会迅速感受到它带来的便利。
更多推荐
所有评论(0)