uiautomator2 Detailed explanation: Say goodbye to repetitive mobile phone operations!

As an Android user or test engineer, do you often get stuck in these repetitive tasks:

  • When testing new functions, the same registration, login, and form-filling processes need to be manually completed a hundred times;
  • Open a bunch of apps every day to check in, sign in, and do tasks, which is so repetitive that you doubt your life;
  • I didn’t want to root my phone, but I also wanted to use code to control the Android interface, but I was dissuaded by Google’s native uiautomator.

ByteDance's Python lightweight automation library uiautomator2, which is based on Android's native UIAutomator secondary encapsulation, just solves these problems - No root required, non-intrusive, and extremely fast to get started. It can not only meet the needs of daily UI automation testing, but also serve as a basic tool for personal learning or lightweight automation (please be sure to use it within the legal scope).

This article will take you from Minimalist environment-setupCore Tool Class EncapsulationDouyin Information Flow Simulation PracticePitfall Avoidance and Anti-Detection Suggestions, and it will take you one hour to get this practical tool.


1. Basic configuration: two-step installation

1.1 Install Python core libraries and auxiliary tools

Domestic users give priority to Tsinghua Source or Douban Source, and the download speed can be more than 10 times faster:

# 安装核心库
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 The device is connected for the first time (only once)

This step requires you to do a little operation on your mobile phone, and then the computer will remember the configuration and connect automatically throughout the process.

  1. Enable developer options and debugging permissions Open your phone Settings → About Phone, click "Version Number" 7 times in succession to enter developer mode. Then go to Settings → Developer Options and turn on "USB Debugging" and "USB Installation". If you need to test functions such as maps, you can also turn on "Allow simulated locations".

  2. Connect the computer with a data cable Select File Transfer (MTP) mode when connecting. Open a command line to check if adb has recognized the device:

    adb devices

If you see the device serial number, the connection is successful.

  1. Run the automatic push service for the first time The first time you connect a device in Python, uiautomator2 will automatically push and install two key components to the phone:
    • ATX-Agent:Service management program on the device side
    • uiautomator2-server: Core service responsible for UI interaction

⚠️ Important reminder: After the installation is complete, you must manually allow the floating window permissions of ATX-Agent on your mobile phone. Systems such as MIUI, ColorOS, Funtouch OS often need to be opened separately in "Application Permissions", otherwise elements may not be positioned properly.


2. Core tool class encapsulation: write once and use it everywhere

The API that comes with uiautomator2 is already very friendly, but if each script has to repeatedly write device connection, element search, and gesture sliding, the code will become more and more bloated. We can encapsulate these high-frequency operations into a lightweight tool classU2Controller, greatly improving development efficiency.

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}")

Usage Suggestions:

  • Single device testing, directlyctrl = U2Controller()That’s it;
  • Multiple devices in parallel, pass in the serial numberctrl = U2Controller("设备序列号"), each instance independently controls a mobile phone.

3. Practical demonstration: Douyin lightweight information flow interaction

Next, an example close to a real scenario will be used to demonstrate the practical capabilities of the above tools - Simulating users to browse TikTok, like randomly, and occasionally comment. Be sure to use it only for automated testing or non-profit learning.

3.1 First use weeditor to figure out the interface layout

Start weeditor:

weditor

The browser will automatically open a page. After selecting the corresponding device, you can see the real-time screen of the mobile phone and the complete UI hierarchy tree. We can view the control structure of Douyin through weeditor, but in order to adapt to different screens and version updates, our script will give priority to using a combination of proportional coordinates and attribute positioning.

3.2 Write automated interaction scripts

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. Pitfall avoidance and anti-detection guide

4.1 Common pitfalls and solutions

  1. Element positioning priorityresourceId(unique, does not change with UI revision or multi-language) ✅ description(Accessible text) 👉 text(Multiple languages ​​or UI revision may change) 👉 class_name(easy to repeat) ⚠️ xpath(Performance is slightly lower, positioning may not be accurate in long lists) Give priority to using the first two, which are stable and efficient.

  2. Floating window permission Be sure to make sure that the floating window permission of ATX-Agent is turned on, otherwise many positioning methods that rely on accessibility services will fail.

  3. Multiple Device Management passadb devicesObtain the device serial number and create a separateU2Controllerinstances to avoid interfering with each other.

  4. Network Fluctuation In actual combat, try to use explicit wait (for exampleelement.wait()) instead of fixedtime.sleep(), to avoid operation failure due to network lag.

4.2 Basic anti-detection ideas (for learning reference only)

If you use these scripts in non-profit learning or automated testing, you can add the following "anthropomorphic" operations to reduce the probability of being recognized as a machine:

  • Randomize everything: viewing duration, sliding distance, click coordinates, operation intervals, all added to random fluctuations.
  • Diversified Behaviors: Don’t like continuously, don’t scroll down all the time, occasionally enter the comment area only to read, and occasionally switch to the background and come back.
  • Reasonable frequency control: Do not run 24 hours a day, simulate normal work and rest, and control it within a reasonable time period every day.

All code and technical ideas provided in this article are only used for legitimate Android application automation testing and non-profit personal learning research. Please do not use it for any malicious brushing, false data, illegal collection or other illegal activities, otherwise all consequences arising therefrom will be borne by the user.