uiautomator2详解:告别重复的手机操作!

作为一个 Android 用户或测试工程师,你是不是也经常陷在这些重复劳动里:

  • 测试新功能时,同一套注册、登录、填表单流程要手动走一百遍;
  • 每天打开一堆 App 打卡、签到、做任务,重复到怀疑人生;
  • 不想 Root 手机,又想用代码控制安卓界面,结果被 Google 原生的 uiautomator 劝退。

字节跳动基于安卓原生 UIAutomator 二次封装的 Python 轻量级自动化库 uiautomator2,正好解决了这些问题——无需 Root、非侵入式、上手极快。它既能满足日常 UI 自动化测试的需求,也能作为个人学习或轻量自动化的基础工具(请务必在合法范围内使用)。

这篇文章会带你从 极简environment-setup核心工具类封装抖音信息流模拟实战避坑与反检测建议,用一小时拿下这个实用工具。


一、基础配置:两步搞定安装

1.1 安装 Python 核心库与辅助工具

国内用户优先使用清华源或豆瓣源,下载速度能快 10 倍以上:

# 安装核心库
pip install uiautomator2 -i https://pypi.tuna.tsinghua.edu.cn/simple

# 强烈推荐安装:UI 元素定位辅助工具 weditor
pip install weditor -i https://pypi.tuna.tsinghua.edu.cn/simple

1.2 设备首次连接(仅需一次)

这一步需要你在手机上稍微操作一下,之后电脑会记住配置,全程自动连接。

  1. 开启开发者选项与调试权限
    打开手机 设置 → 关于手机,连续点击 “版本号” 7 次,进入开发者模式。
    然后进入 设置 → 开发者选项,开启 “USB 调试”“USB 安装”,如果需要测试地图等功能,可以顺带打开 “允许模拟位置”

  2. 用数据线连接电脑
    连接时选择 “文件传输”(MTP)模式。打开命令行检查 adb 是否已识别设备:

    adb devices

    如果看到设备序列号,表示连接成功。

  3. 首次运行自动推送服务
    第一次在 Python 中连接设备时,uiautomator2 会自动向手机推送并安装两个关键组件

    • ATX-Agent:设备端的服务管理程序
    • uiautomator2-server:负责 UI 交互的核心服务

    ⚠️ 重要提醒:安装完成后,一定要在手机上手动允许 ATX-Agent 的悬浮窗权限。MIUI、ColorOS、Funtouch OS 等系统往往需要去“应用权限”里单独开启,否则可能出现无法正常定位元素的情况。


二、核心工具类封装:写一次,到处用

uiautomator2 自带的 API 已经非常友好,但如果每个脚本都要重复写设备连接、元素查找、手势滑动,代码会越来越臃肿。我们可以把这些高频操作封装成一个轻量级工具类 U2Controller,大幅提升开发效率。

import uiautomator2 as u2
import time
import random
from typing import Optional, List

class U2Controller:
    """轻量级 uiautomator2 工具类:支持多设备、元素定位、手势操作、设备管理"""

    def __init__(self, device_id: Optional[str] = None):
        """
        初始化设备连接
        :param device_id: 可选,多设备时传入 USB 序列号(通过 adb devices 查看),单设备直接留空
        """
        self.device_id = device_id
        self.d = None
        self._connect_device()

    def _connect_device(self):
        """内部方法:建立与设备的通信"""
        try:
            self.d = u2.connect(self.device_id) if self.device_id else u2.connect()
            print(f"✅ 设备连接成功:序列号={self.d.serial},屏幕尺寸={self.d.window_size()}")
            return True
        except Exception as e:
            print(f"❌ 设备连接失败:{str(e)}")
            return False

    # -------------------------- 核心元素定位与点击 --------------------------
    def click_element(self,
                     locator_type: str,
                     locator_value: str,
                     timeout: int = 3) -> bool:
        """
        按指定方式定位并点击元素
        :param locator_type: ✅推荐 resource_id / description,其次 text / class_name,⚠️最后选 xpath(性能略低)
        :param locator_value: 定位值
        :param timeout: 等待元素出现的超时时间(秒)
        :return: 是否点击成功
        """
        locator_map = {
            'resource_id': self.d(resourceId=locator_value),
            'description': self.d(description=locator_value),
            'text': self.d(text=locator_value),
            'class_name': self.d(className=locator_value),
            'xpath': self.d.xpath(locator_value)
        }
        element = locator_map.get(locator_type, self.d(resourceId=locator_value))

        if element.exists(timeout=timeout):
            element.click()
            print(f"✅ 点击元素:{locator_type}={locator_value}")
            return True
        print(f"⚠️ 未找到元素:{locator_type}={locator_value}")
        return False

    def scroll_to_element(self,
                          locator_type: str,
                          locator_value: str,
                          direction: str = "up",
                          max_scrolls: int = 5) -> bool:
        """
        滚动屏幕直到找到目标元素(适合长列表、长页面)
        :param direction: 滚动方向,up / down
        :param max_scrolls: 最大滚动次数
        :return: 是否找到元素
        """
        for _ in range(max_scrolls):
            if self.click_element(locator_type, locator_value, timeout=1):
                return True
            self.d(scrollable=True).scroll(direction)
            time.sleep(0.5)
        print(f"⚠️ 滚动{max_scrolls}次后未找到元素")
        return False

    # -------------------------- 屏幕比例手势(适配不同机型) --------------------------
    def swipe_up(self,
                start_ratio: float = 0.8,
                end_ratio: float = 0.2,
                duration: float = 0.5,
                jitter: bool = True):
        """
        向上滑动(基于屏幕比例 + 随机抖动,模拟真人操作)
        :param start_ratio: 滑动起始 y 轴位置占屏幕高度的比例(0-1)
        :param end_ratio: 滑动结束 y 轴位置占屏幕高度的比例
        :param duration: 滑动持续时间(秒)
        :param jitter: 是否加入随机抖动
        """
        w, h = self.d.window_size()
        start_x = w // 2 + (random.randint(-10, 10) if jitter else 0)
        start_y = h * start_ratio + (random.randint(-20, 20) if jitter else 0)
        end_x = w // 2 + (random.randint(-10, 10) if jitter else 0)
        end_y = h * end_ratio + (random.randint(-20, 20) if jitter else 0)

        self.d.swipe(start_x, start_y, end_x, end_y, duration)
        print("✅ 执行向上滑动")

    # -------------------------- 基础设备管理 --------------------------
    def take_screenshot(self, save_path: Optional[str] = None) -> str:
        """截图并返回保存路径"""
        if not save_path:
            save_path = f"screenshot_{int(time.time())}.png"
        self.d.screenshot(save_path)
        print(f"📸 截图已保存:{save_path}")
        return save_path

    def press_system_key(self, key: str = "back"):
        """按下 Android 系统键(back / home / recent / volume_up 等)"""
        self.d.press(key)
        print(f"🔘 按下系统键:{key}")

使用建议

  • 单设备测试,直接 ctrl = U2Controller() 即可;
  • 多设备并行,传入序列号 ctrl = U2Controller("设备序列号"),每个实例独立操控一台手机。

三、实战演示:抖音轻量级信息流交互

接下来用一个贴近真实场景的例子,展示上面工具类的实战能力——模拟用户刷抖音、随机点赞、偶尔评论。请务必仅用于自动化测试或非盈利学习。

3.1 先用 weditor 摸清界面布局

启动 weditor:

weditor

浏览器会自动打开一个页面,选择对应设备后,就可以看到手机实时画面完整的 UI 层级树。我们可以通过 weditor 查看抖音的控件结构,但为了适配不同屏幕和版本更新,我们的脚本会优先使用比例坐标属性定位相结合的方式。

3.2 编写自动化交互脚本

class DouyinAutoFlow:
    """抖音轻量级信息流交互类"""

    def __init__(self, device_id: Optional[str] = None):
        self.ctrl = U2Controller(device_id)
        self.d = self.ctrl.d
        self.package_name = "com.ss.android.ugc.aweme"

    def launch_app(self):
        """冷启动抖音并等待加载"""
        try:
            self.d.app_start(self.package_name, stop=True)
            print("📱 正在冷启动抖音,请稍候...")
            time.sleep(random.uniform(6, 9))  # 模拟真实用户的冷启动等待
            return True
        except Exception as e:
            print(f"❌ 启动抖音失败:{str(e)}")
            return False

    def random_watch(self, cycles: int = 5):
        """
        按真实用户行为权重,随机执行观看、点赞、评论、滑动
        :param cycles: 交互循环次数
        """
        if not self.d:
            return
        print("🤖 开始模拟抖音信息流观看...")

        for i in range(cycles):
            print(f"\n🔄 第 {i+1} 次循环")
            # 模拟真实观看停留时长(2~6 秒)
            watch_time = random.uniform(2, 6)
            print(f"⏳ 观看当前视频 {watch_time:.1f} 秒")
            time.sleep(watch_time)

            # 随机选择一个动作:点赞 / 滑动 / 评论,概率分别为 75% / 20% / 5%
            action = random.choices(
                ['like', 'scroll', 'comment'],
                weights=[7.5, 2, 0.5]
            )[0]

            if action == 'like':
                self._like_video_by_ratio()
            elif action == 'comment':
                self._simple_random_comment()

            # 无论是否执行点赞或评论,90% 的概率滑动到下一个视频
            if random.random() < 0.9:
                self.ctrl.swipe_up()
                time.sleep(random.uniform(0.8, 1.5))

    def _like_video_by_ratio(self):
        """通过屏幕比例定位右侧点赞区域,适配不同屏幕"""
        w, h = self.d.window_size()
        like_x = w * 4 // 5 + random.randint(-15, 15)
        like_y = h // 2 + random.randint(60, 100)
        self.d.click(like_x, like_y)
        print("❤️ 已点赞当前视频")

    def _simple_random_comment(self):
        """发送一条简单评论,失败时自动退出评论区"""
        try:
            w, h = self.d.window_size()
            # 点击右侧评论图标
            comment_x = w * 4 // 5 + random.randint(-15, 15)
            comment_y = h // 2 + random.randint(200, 240)
            self.d.click(comment_x, comment_y)
            time.sleep(random.uniform(1.5, 2.5))

            # 随机输入一句评论
            preset_comments = ["不错👍", "好看好看", "学到了!", "666", "哈哈哈哈"]
            input_text = random.choice(preset_comments)
            self.d.send_keys(input_text)
            time.sleep(random.uniform(0.5, 1))

            # 点击发送(优先通过 text 属性定位)
            send_btn = self.d(text="发送") or self.d.xpath('//*[@text="发送"]')
            if send_btn.exists(timeout=2):
                send_btn.click()
                print(f"💬 已发送评论:{input_text}")

            # 退出评论区
            self.ctrl.press_system_key()
            time.sleep(random.uniform(0.8, 1.2))
        except Exception as e:
            print(f"⚠️ 评论执行异常:{str(e)}")
            self.ctrl.press_system_key()
            time.sleep(0.5)

    def stop_app(self):
        """停止抖音"""
        self.d.app_stop(self.package_name)
        print("📱 抖音已关闭")

# -------------------------- 运行演示 --------------------------
if __name__ == "__main__":
    flow = DouyinAutoFlow()
    if flow.launch_app():
        try:
            flow.random_watch(cycles=4)
        except KeyboardInterrupt:
            print("\n⏸️ 用户主动中断运行")
        finally:
            flow.stop_app()

四、避坑与反检测指南

4.1 常见坑点与解决方法

  1. 元素定位优先级
    resourceId(唯一,不随 UI 改版或多语言变化)
    description(无障碍文本)
    👉 text(多语言或 UI 改版可能变化)
    👉 class_name(容易重复)
    ⚠️ xpath(性能略低,长列表中可能定位不准)
    优先使用前两种,稳定又高效。

  2. 悬浮窗权限
    务必确保 ATX-Agent 的悬浮窗权限已经开启,否则很多依靠无障碍服务的定位方式会失效。

  3. 多设备管理
    通过 adb devices 获取设备序列号,为每个设备创建独立的 U2Controller 实例,避免互相干扰。

  4. 网络波动
    实战中尽量使用显式等待(例如 element.wait())代替固定 time.sleep(),避免因网络卡顿导致操作失败。

4.2 基础反检测思路(仅供学习参考)

如果你在非盈利学习或自动化测试中使用这些脚本,可以加上以下“拟人化”操作,降低被识别为机器的概率:

  • 随机化一切:观看时长、滑动距离、点击坐标、操作间隔,全部加入随机波动。
  • 行为多样化:不要连续点赞、不要一直往下刷,偶尔进入评论区只看不发,偶尔切到后台再回来。
  • 合理控制频率:别 24 小时不间断运行,模拟正常作息,每天控制在合理的时间段内。

五、合法声明

本文提供的所有代码和技术思路,仅限用于 合法的 Android 应用自动化测试非盈利性的个人学习研究。请勿将其用于任何恶意刷量、虚假数据、违规采集或其他违法行为,否则由此产生的一切后果由使用者自行承担。