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

第一次运行会自动安装并尝试在浏览器中打开界面。但这里常见的问题有:

  1. 端口冲突 :默认使用 17310 端口。如果该端口被占用,启动会失败。可以通过 --port 参数指定其他端口,如 python -m weditor --port 8090
  2. 浏览器无法自动打开 :这没关系,命令行会输出一个本地 URL(如 http://localhost:17310 ),手动在浏览器中打开即可。
  3. 连接设备失败 :确保 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 连接、元素定位稳定性等挑战,但一旦掌握了这些问题的应对方法,构建稳定可靠的自动化任务就变成了一件充满成就感的事情。记住,好的自动化脚本不是一蹴而就的,它需要你在实际运行中不断观察、调试和优化。开始动手写你的第一个脚本吧,从自动给某个应用签到开始,你会迅速感受到它带来的便利。

更多推荐