
1. 项目概述为什么选择uiautomator2如果你正在为Android应用的自动化测试或脚本化操作寻找一个趁手的工具那么uiautomator2绝对是一个绕不开的名字。它不是一个全新的发明而是对Android官方提供的UI自动化测试框架uiautomator的一次强力封装和增强。简单来说它让原本需要Java编写、依赖Android SDK、配置繁琐的自动化任务变得可以用Python轻松驾驭。想象一下你只需要写几行Python代码就能控制手机自动安装应用、点击屏幕、输入文字、滑动列表甚至获取控件信息进行断言这对于做自动化测试、数据抓取、或者批量执行重复性手机操作的人来说效率提升是颠覆性的。我最初接触它是因为需要批量测试公司产品在不同机型上的兼容性。手动操作几十台手机那简直是噩梦。尝试过一些录制回放工具但灵活性和可维护性太差。直到用了uiautomator2我才真正把“自动化”落到了实处。它的核心优势在于直接与Android系统底层的AccessibilityService和uiautomator服务通信稳定性和速度都比基于图像识别的方案高得多。更重要的是它用Python作为脚本语言生态丰富你可以轻松地结合pytest组织测试用例用Allure生成漂亮报告或者集成到CI/CD流水线中。接下来我会带你从零开始搭建一个坚实可靠的uiautomator2环境并用一个完整的例子演示如何让它为你工作。2. 环境搭建全流程与核心原理剖析搭建uiautomator2的环境不仅仅是安装几个Python包那么简单。它涉及PC端你的开发机和移动端Android设备或模拟器的协同工作。理解这个通信流程对于后续排查问题至关重要。2.1 PC端环境准备Python与ADB的基石PC端是我们的指挥中心需要两个核心工具Python和ADB。Python环境uiautomator2是一个Python库因此一个干净的Python环境是首要条件。我强烈建议使用Python 3.7及以上版本。为了避免与系统或其他项目的Python包冲突使用虚拟环境virtual environment是业界最佳实践。这能确保你的依赖库版本纯净、可复现。# 创建并激活一个名为u2env的虚拟环境 python -m venv u2env # Windows系统激活 u2env\Scripts\activate # macOS/Linux系统激活 source u2env/bin/activate激活后你的命令行提示符前会出现(u2env)表示你已进入该虚拟环境。ADB工具ADB是Android Debug Bridge的缩写它是PC与Android设备通信的桥梁。uiautomator2的初始化、应用安装、设备连接等底层操作都依赖于ADB。你需要从Android SDK的platform-tools中获取ADB或者直接下载独立的平台工具包。将其路径添加到系统的环境变量PATH中是保证在任何命令行窗口都能调用adb命令的关键。注意很多连接问题都源于ADB版本过旧或与设备不兼容。确保你的ADB版本相对较新。可以通过adb version命令查看。2.2 移动端环境部署atx-agent守护进程这是uiautomator2设计的精妙之处。传统的uiautomator测试需要将测试包打包进APK安装到设备再执行流程笨重。uiautomator2在设备端植入了一个轻量的守护进程——atx-agent。当你第一次在PC端执行python -m uiautomator2 init时会发生以下事情PC端的脚本通过ADB检测连接的设备。将atx-agent的安装包推送到设备的/data/local/tmp目录。在设备上启动atx-agent服务。这个服务会在设备上开启一个HTTP接口默认端口7912。后续你的Python代码就不再直接通过ADB发送复杂的uiautomator命令而是向这个HTTP接口发送简单的RESTful API请求如GET /click由atx-agent在设备本地解析并调用真正的uiautomator API来执行操作。这种“PC发送指令设备端代理执行”的架构带来了几个巨大好处速度更快本地调用避免ADB命令行开销稳定性更高减少了ADB连接不稳定的影响功能更强可以方便地扩展HTTP接口实现更多功能如截图、推送文件。2.3 核心库安装与初始化在准备好的Python虚拟环境中安装uiautomator2库非常简单pip install uiautomator2这个命令会安装uiautomator2库及其依赖如requests,progress等。安装完成后不要急着写代码先进行设备初始化。将你的Android设备通过USB线连接电脑并开启USB调试模式在“设置”-“关于手机”中连续点击“版本号”7次开启开发者选项然后在开发者选项中开启“USB调试”。在命令行执行初始化python -m uiautomator2 init你会看到类似以下的输出表示正在向设备安装必要的组件[I 230101 10:00:00 init:144] Device serial: xxxxxx [I 230101 10:00:00 init:145] Installing minicap, minitouch, atx-agent [I 230101 10:00:05 init:158] Initialization finished.实操心得init过程可能会因为网络问题需要从GitHub下载组件而失败。如果遇到超时可以尝试使用--mirror参数指定国内镜像源例如python -m uiautomator2 init --mirror https://mirrors.aliyun.com/pypi/simple/。初始化成功后设备上会多出一个名为com.github.uiautomator的应用这就是atx-agent的载体切勿卸载。3. 连接设备多种方式与深度解析环境搭建好后第一步就是让Python脚本与设备建立连接。uiautomator2提供了多种连接方式适应不同场景。3.1 基于设备序列号Serial的USB连接这是最常用、最稳定的方式。首先通过adb devices命令获取设备的序列号。adb devices List of devices attached abcdefg123456 device然后在Python代码中使用这个序列号进行连接import uiautomator2 as u2 # 方法1使用设备序列号 d u2.connect(abcdefg123456) # 方法2连接当前通过USB连接的唯一设备当只有一台设备时最方便 d u2.connect()为什么推荐使用序列号在同时连接多台设备进行测试时序列号是唯一标识可以精准控制目标设备避免操作错乱。3.2 基于设备IP地址的无线连接Wi-Fi Debugging无线连接让你摆脱线缆束缚非常适合手机固定在支架上进行长时间自动化测试的场景。前提是设备与PC在同一个局域网内并且已经在设备上开启了“无线调试”选项Android 11及以上或通过USB完成了一次ADB TCP/IP连接授权。步骤通常如下USB连接设备执行adb tcpip 55555555是端口号。拔掉USB线。在设备“设置”-“关于手机”-“状态信息”中查看设备的IP地址如192.168.1.100。在PC上执行adb connect 192.168.1.100:5555。连接成功后在代码中即可使用IP连接。import uiautomator2 as u2 # 使用设备IP地址和端口进行连接 d u2.connect(192.168.1.100:5555)注意事项无线连接虽然方便但稳定性不如USB可能受网络波动影响。在进行关键测试或需要稳定截图的场景下优先使用USB连接。另外每次设备重启后无线调试端口可能会关闭需要重新用USB执行adb tcpip命令。3.3 连接Android模拟器对于没有真机的开发者模拟器是绝佳的替代品。主流模拟器如Google官方AVD、夜神模拟器、MuMu模拟器等都支持。连接模拟器通常需要使用特殊的ADB连接地址。例如对于Android Studio的AVD默认的连接地址是emulator-5554。你可以在命令行用adb devices查看模拟器的序列号它通常以emulator-开头。import uiautomator2 as u2 # 连接名为Pixel_4_API_30的AVD模拟器 d u2.connect(emulator-5554) # 或者直接使用u2.connect_usb(emulator-5554)明确USB连接方式模拟器避坑指南性能确保为模拟器分配足够的内存和CPU资源否则自动化操作会非常卡顿。渲染模式如果遇到截图花屏或识别不到控件的问题尝试在模拟器设置中将“图形”Graphics选项从“自动”或“硬件”切换到“软件”渲染模式。桥接模式无线连接模拟器时确保模拟器的网络模式是桥接Bridged使其与PC处于同一局域网段才能获得一个可路由的IP地址。4. 核心API详解与控件定位策略成功连接设备后我们就获得了操作设备的“遥控器”——d对象。接下来学习如何用它来“点击”和“输入”。4.1 基础操作点击、输入与滑动这些是构成任何自动化脚本的原子操作。点击操作最核心的操作。uiautomator2提供了多种点击方式。# 1. 通过控件的resource-id、text等属性定位后点击最常用 d(resourceIdcom.android.settings:id/search_action_bar).click() # 2. 通过坐标点击不推荐兼容性差 d.click(x, y) # 3. 点击屏幕上出现的特定文字 d(text设置).click() # 4. 长按 d(text应用).long_click()输入文本通常用于填写表单。# 1. 先定位到输入框控件再调用set_text方法 d(resourceIdcom.example.app:id/username_input).set_text(my_username) # 2. 直接对当前焦点控件输入风险较高需确保焦点正确 d.set_fastinput_ime(True) # 启用快速输入法绕过系统输入法更快 d.send_keys(password123) d.set_fastinput_ime(False) # 操作完成后关闭滑动操作用于翻页、滚动列表。# 从屏幕坐标(x1, y1)滑动到(x2, y2)持续0.5秒 d.swipe(x1, y1, x2, y2, 0.5) # 更常用的方向性滑动 d.swipe_ext(up) # 向上滑动 d.swipe_ext(down) # 向下滑动 d.swipe_ext(left) # 向左滑动 d.swipe_ext(right) # 向右滑动4.2 控件定位Selector选择器的艺术能否稳定地定位到目标控件是自动化脚本成败的关键。uiautomator2使用Selector选择器来定位控件其语法非常灵活。核心定位属性text控件的文本内容。如d(text确定)。resourceId控件的资源ID通常是package名:id/控件名的形式。这是最稳定、最优先使用的属性。如d(resourceIdcom.tencent.mm:id/baj)。className控件的类名如android.widget.TextView。description/content-desc控件的描述信息常用于无障碍访问。packageName控件所在应用的包名用于限定搜索范围。组合定位与关系定位 当单一属性无法唯一定位时可以组合使用或者利用控件间的层级关系。# 组合定位同时满足resourceId和text d(resourceIdcom.android.settings:id/title, textWLAN).click() # 父子关系定位先定位父控件再在其中找子控件 parent d(classNameandroid.widget.ListView) child parent.child(text蓝牙) child.click() # 兄弟关系定位使用instance索引从0开始 # 点击第三个文本为“确定”的按钮 d(text确定, instance2).click()动态内容定位技巧 对于文本内容会变化的控件如聊天消息避免直接使用text定位。可以结合其他不变属性如resourceId、className或者使用文本匹配。# 使用正则表达式匹配部分文本 d(textMatches.*你好.*).click() # 使用描述信息定位 d(description更多选项).click()实操心得如何获取控件属性这是新手最大的障碍。强烈推荐使用uiautomator2自带的weditor工具。在命令行运行python -m weditor会自动打开浏览器。在页面中选择你的设备就能实时看到手机屏幕的UI层级树点击任意控件其resourceId、text、bounds等所有属性一目了然。这是编写选择器的“瑞士军刀”。5. 完整例子演示自动化操作“设置”应用理论说得再多不如一个实实在在的例子。让我们编写一个脚本自动完成以下任务打开手机“设置” - 进入“WLAN”设置 - 打开WLAN开关 - 然后返回到设置主页。import uiautomator2 as u2 import time # 1. 连接设备这里以连接唯一USB设备为例 d u2.connect() # 2. 启动“设置”应用 # 方式A通过包名和活动名启动最精确 d.app_start(com.android.settings, .Settings) # 方式B如果不知道活动名可以直接用包名u2会尝试启动主活动 # d.app_start(com.android.settings) print(已启动设置应用) time.sleep(2) # 等待应用完全启动 # 3. 定位并点击“网络和互联网”或“WLAN”选项 # 不同品牌手机、不同Android版本的设置界面布局可能不同。 # 这里提供两种常见的定位策略 try: # 策略1优先通过resource-id定位最稳定 network_item d(resourceIdandroid:id/title, textMatches.*网络.*|.*WLAN.*|.*Wi-Fi.*) if network_item.exists: network_item.click() print(点击进入网络设置) else: # 策略2如果策略1失败尝试通过文本直接定位 d(textWLAN).click() print(直接点击WLAN进入) except Exception as e: print(f定位网络设置项失败: {e}) # 可以在这里加入截图方便事后分析 d.screenshot(error_network_entry.png) exit(1) time.sleep(1.5) # 等待WLAN设置页面加载 # 4. 在WLAN页面找到开关控件并打开 # 开关通常是一个Switch控件可以用className或resourceId定位 try: # 常见Switch的类名 wlan_switch d(classNameandroid.widget.Switch) if wlan_switch.exists: current_state wlan_switch.info[checked] print(f当前WLAN开关状态: {current_state}) if not current_state: wlan_switch.click() print(已打开WLAN开关) else: print(WLAN开关已是打开状态) else: # 有些厂商的开关可能是CheckBox或其他自定义控件 # 尝试通过旁边的文本来定位然后点击其相邻的开关区域 d(textWLAN).sibling(classNameandroid.widget.Switch).click() print(通过兄弟节点定位并点击开关) except Exception as e: print(f操作WLAN开关失败: {e}) d.screenshot(error_wlan_switch.png) time.sleep(1) # 等待开关状态切换 # 5. 按返回键回到设置主页 d.press(back) print(按返回键退出WLAN设置) time.sleep(0.5) # 6. 再次按返回键退出设置应用可选 # d.press(back) print(自动化脚本执行完毕) # 7. 停止应用可选清理现场 # d.app_stop(com.android.settings)脚本逻辑深度解析健壮性处理脚本中使用了try...except块来捕获可能出现的定位失败异常并保存了错误时的屏幕截图。这是编写生产级自动化脚本的必备习惯。等待策略在关键操作如启动应用、点击进入新页面后使用time.sleep进行固定等待。这是一种简单策略但在复杂场景下可能不够。更优的做法是使用显式等待等待某个特定控件出现后再进行下一步这能大大提高脚本的稳定性和执行速度。兼容性考虑不同手机厂商小米、华为、三星等对系统应用的UI定制差异很大。脚本中提供了多种定位策略resourceId、text、className、sibling并优先使用最稳定的resourceId同时用textMatches进行模糊匹配以增加脚本在不同设备上的适应性。状态判断在操作开关前先通过info[checked]获取了当前状态避免了不必要的重复操作并使脚本逻辑更清晰。6. 高级特性与实战技巧掌握了基础操作和定位你已经能完成80%的自动化任务。剩下的20%则需要一些高级特性和技巧来应对复杂场景。6.1 等待机制让脚本更稳定time.sleep是“笨等待”效率低。uiautomator2提供了更智能的等待。隐式等待为所有控件查找操作设置一个全局超时时间。d.implicitly_wait(10.0) # 设置隐式等待10秒 # 此后任何 d(...) 查找操作如果找不到控件都会等待最多10秒期间不断重试直到找到或超时显式等待等待某个特定条件成立这是最推荐的方式。from uiautomator2 import Until # 等待“确定”按钮出现最多等10秒 d.wait(Until.find_element(text确定), timeout10.0).click() # 等待当前活动页面是指定的活动 d.wait_activity(.MainActivity, timeout10) # 自定义等待条件等待某个控件文本变为“完成” def text_is_done(el): return el.info.get(text) 完成 d.wait(text_is_done, timeout15)6.2 监控与钩子Hook处理弹窗与异常自动化过程中最讨厌的就是突如其来的弹窗权限申请、升级提示、广告。我们可以通过注册“监视器”Watcher来自动处理它们。# 注册一个监视器命名为“处理权限弹窗” d.watcher(处理权限弹窗).when( text允许 # 当屏幕上出现“允许”按钮时 ).click( # 执行点击操作 text允许 # 点击这个“允许”按钮 ) # 再注册一个处理“确定”按钮 d.watcher(处理确定弹窗).when(text确定).click(text确定) # 启动所有已注册的监视器 d.watchers.start() # 在脚本主循环中定期检查并触发监视器 # 通常可以放在一个循环中或者使用d.watchers.run()手动触发 import time while True: d.watchers.run() # 运行一次监视器检查 time.sleep(1) # 每秒检查一次钩子Hook可以在操作执行前后插入自定义逻辑非常适合用于日志记录或错误恢复。def before_click(el): print(f[Hook] 即将点击控件: {el.info.get(text)}) def after_click(el): print(f[Hook] 点击完成) # 注册钩子 d.hook_click(beforebefore_click, afterafter_click) # 此后每次执行click()都会打印日志6.3 图像识别辅助定位尽管优先使用控件定位但有些场景如游戏界面、自定义绘制控件无法获取控件信息。这时可以结合图像识别。uiautomator2本身不包含复杂的图像识别算法但可以轻松地与opencv-python等库结合。基本思路是用d.screenshot()截图然后用OpenCV模板匹配在截图中查找预设的按钮图片。import cv2 import numpy as np # 1. 截取当前屏幕 screen d.screenshot() screen_cv cv2.cvtColor(np.array(screen), cv2.COLOR_RGB2BGR) # 2. 读取要查找的模板图片小图 template cv2.imread(button_template.png) h, w template.shape[:2] # 3. 进行模板匹配 result cv2.matchTemplate(screen_cv, template, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc cv2.minMaxLoc(result) # 4. 如果匹配度足够高则计算中心点并点击 threshold 0.8 # 置信度阈值 if max_val threshold: center_x max_loc[0] w // 2 center_y max_loc[1] h // 2 d.click(center_x, center_y) print(f通过图像识别点击坐标 ({center_x}, {center_y})) else: print(未找到目标按钮)注意事项图像识别受屏幕分辨率、缩放、色差影响大且执行速度较慢。应仅作为控件定位失效时的补充手段。7. 常见问题排查与性能优化实录即使按照教程一步步来在实际操作中你还是会遇到各种各样的问题。这里我整理了最常遇到的“坑”和解决方法。7.1 连接与初始化问题问题1执行python -m uiautomator2 init失败提示网络错误或超时。原因脚本需要从GitHub下载atx-agent等组件国内网络可能不稳定。解决使用--mirror参数指定国内镜像python -m uiautomator2 init --mirror https://mirrors.aliyun.com/pypi/simple/。手动下载组件从uiautomator2的GitHub release页面下载对应的atx-agent、minicap、minitouch文件放置到~/.uiautomator2/目录下对应位置再执行init。问题2设备连接成功但执行任何操作都报错或没反应。原因A设备端的atx-agent服务没有正常运行。排查在PC命令行执行adb shell ps | grep atx-agent查看进程是否存在。执行adb shell netstat -tlnp | grep 7912查看7912端口是否在监听。解决重启设备上的服务。可以执行adb shell /data/local/tmp/atx-agent server --stop然后adb shell /data/local/tmp/atx-agent server -d重新启动。或者直接python -m uiautomator2 init重新初始化。原因BPC端Python代码连接的设备序列号或IP与实际不符。解决仔细核对adb devices列出的设备序列号或确认无线连接的IP和端口是否正确。7.2 控件定位失败问题问题3用weditor能看到控件但代码里用d(text“xxx”)就是找不到。原因A控件文本是动态生成的或者包含不可见字符。解决使用textMatches进行正则匹配或者改用resourceId、className等更稳定的属性。在weditor中仔细查看控件的完整text属性值。原因B页面尚未加载完成就执行了查找操作。解决在查找前增加等待。使用d.wait(Until.find_element(...))显式等待控件出现。原因C控件不在当前屏幕可见范围内如在ListView需要滚动才能看到。解决先执行滚动操作d(scrollableTrue).scroll.to(text目标文本)将控件滚动到视图中再操作。问题4点击操作执行了但似乎没效果如按钮没反应。原因A点击的坐标或控件并非真正的可点击区域。有些按钮看起来是一个整体但可点击区域可能只是其中的图标或文字部分。解决在weditor中查看控件的bounds属性确认其点击范围。尝试点击其父控件或子控件。或者使用d.click(x, y)通过截图工具获取更精确的坐标。原因B点击速度太快应用来不及响应。解决在点击后增加一个短暂的等待time.sleep(0.5)。或者使用d.click(持续时间1.0)进行长按式点击如果控件支持。7.3 性能优化与脚本健壮性技巧1减少不必要的截图和属性获取。d.screenshot()和el.info都是相对耗时的操作尤其是在循环中。尽量避免在循环内频繁使用。如果需要判断状态可以优先使用控件本身的属性如el.info[checked]获取一次后缓存起来。技巧2使用Session模式复用连接。如果你的脚本需要频繁断开重连可以使用session模式它能保持一个更稳定的会话并自动处理一些重连逻辑。import uiautomator2 as u2 sess u2.Session(“设备序列号或IP”) # 创建会话 sess.app_start(“包名”) # 使用会话对象进行操作技巧3编写可配置、可复用的脚本。将设备连接信息、应用包名、关键控件的定位器Selector提取到配置文件如config.yaml或常量文件中。这样当应用更新导致resourceId变化时你只需要修改一个地方而不是搜索替换整个脚本。技巧4集成到测试框架。不要只写零散的脚本。将uiautomator2的操作封装成Page Object模式每个页面是一个类页面的元素和操作是类的方法然后使用pytest或unittest来组织测试用例。这样结构清晰易于维护并且可以方便地生成测试报告。最后自动化测试不是一劳永逸的UI的变动是常态。建立一个快速的反馈机制当脚本失败时能第一时间截图、记录日志并定期Review和更新你的定位策略才是让自动化资产持续产生价值的关键。从一个小例子开始逐步扩展到覆盖核心业务流程的自动化脚本集你会真切感受到它带来的效率解放。