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

作为一个Android用户或测试工程师,你有没有过这种烦恼:

  • 测试新应用要重复走100次相同的注册/登录流程?
  • 每天手动刷一堆APP的签到/金币任务?
  • 没有Root权限,想用原生框架控制手机又嫌Google的uiautomator太复杂?

字节跳动基于原生库封装的Python轻量级自动化库uiautomator2完美解决了这些问题——无需Root、非侵入式、入门门槛极低,既能覆盖UI自动化测试的日常需求,也能作为非盈利性个人学习/轻量自动化的基础工具。

本文会从极简环境搭建核心工具类封装抖音信息流模拟实战避坑反检测指南,帮你1小时上手这个工具。


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

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的悬浮窗权限(部分品牌如小米/OPPO/VIVO需要单独去设置里开)。

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

原生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/volume_down等)"""
        self.d.press(key)
        print(f"🔘 按下系统键:{key}")

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

我们以模拟真实用户观看抖音、随机点赞、偶尔滑动、极少评论为例,展示工具类的实用性(仅用于合法的学习/自动化测试)。

3.1 用weditor定位关键元素

虽然我们会优先用比例点击适配不同屏幕和UI版本,但提前了解抖音的布局逻辑会更稳妥:

# 启动weditor
weditor

打开浏览器后,选择对应的设备,就能看到实时屏幕画面UI层级树了。

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):
        """按比例点击右侧点赞按钮(适配不同屏幕和UI改版)"""
        w, h = self.d.window_size()
        like_x = w * 4 // 5 + random.randint(-15, 15)  # 右侧4/5宽度附近,加x轴抖动
        like_y = h // 2 + random.randint(60, 100)       # 屏幕中间偏上,加y轴抖动
        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>xpath)
            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的悬浮窗权限,否则可能无法正常定位UI元素;
  3. 多设备管理:如果要同时控制多台设备,需要传入每台设备的USB序列号(通过adb devices查看);
  4. 网络波动处理:实战项目中最好加入显式等待(比如用element.wait()代替time.sleep()),避免因网络卡导致的操作失败。

4.2 基础反检测(仅供学习)

如果是用于自动化测试以外的非盈利性学习,建议加入以下操作减少被APP检测的风险:

  1. 随机化所有操作:加入随机观看时长、随机滑动距离/速度、随机抖动的点击位置、随机的等待间隔;
  2. 模拟真实行为:不要连续执行相同动作(比如连续点赞100次)、偶尔切换APP后台、偶尔打开评论区只看不发;
  3. 限制运行频率:不要24小时不间断运行,每天控制在合理的时间范围内。

五、合法声明

本文所有代码仅用于合法的Android应用自动化测试非盈利性个人学习,请勿用于恶意刷量、恶意采集等违规操作,否则后果自负。