poco多线程设备串台问题

适用场景:使用 Airtest + Poco 进行多设备并发 UI 自动化测试
Airtest 通过全局变量 G.DEVICE 管理设备状态,导致多线程并发时出现"指定设备 A,操作却落到设备 B"的串台问题。本文提供通过依赖注入重写核心类的完整解决方案,实现设备实例与线程的绑定隔离。

一、问题描述

在使用 Airtest + Poco 做多设备并发 UI 自动化时,会出现设备串台现象:给 Poco 指定了设备 A,实际点击操作却执行在设备 B 上
典型表现:
  • 多线程同时运行测试,每个线程连接不同设备
  • 线程的 pocoairtest 操作落到其他设备
  • 单线程运行正常,并发即异常

直接上代码

UnityPoco

# -*- coding: utf-8 -*-
# @Time    : 2026/4/23 16:34
# @Author  : Liam Li
# @FileName: PrivateUnityPoco.py
# @Software: PyCharm
# @Description  :线程安全的 UnityPoco 实现,不依赖全局 G.device

from functools import wraps
from poco.drivers.std import StdPoco
from poco.agent import PocoAgent
from poco.drivers.std.attributor import StdAttributor
from poco.drivers.std.dumper import StdDumper
from poco.drivers.std.screen import StdScreen
from poco.freezeui.hierarchy import FrozenUIHierarchy
from poco.sdk.interfaces.input import InputInterface
from poco.utils.simplerpc.rpcclient import RpcClient
from poco.utils.simplerpc.transport.tcp.main import TcpClient
from airtest.core.helper import device_platform, logwrap


def serializable_adapter(func):
    """序列化适配器装饰器"""
    class ISerializable(object):
        def to_json(self):
            raise NotImplementedError

    @wraps(func)
    def wrapped(*args):
        class PocoUIProxySerializable(ISerializable):
            def __init__(self, obj):
                self.obj = obj

            def to_json(self):
                return repr(self.obj)

        new_args = [PocoUIProxySerializable(a) for a in args]
        return func(*new_args)

    return wrapped


@serializable_adapter
@logwrap
def record_ui(driver, action, ui, args):
    """记录 UI 操作"""
    return ui


class SafeAirtestInput(InputInterface):
    """
    线程安全的 AirtestInput 实现
    使用指定的设备实例而不是全局 current_device()
    """
    def __init__(self, device):
        super(SafeAirtestInput, self).__init__()
        self.device = device  # 使用传入的设备实例
        self.default_touch_down_duration = 0.01
        self._driver = None
        self.use_render_resolution = False
        self.render_resolution = None

    def add_preaction_cb(self, driver):
        """添加预操作回调"""
        self._driver = driver
        self._driver.add_pre_action_callback(record_ui)

    def get_target_pos(self, x, y):
        """获取目标位置的实际坐标"""
        offsetx, offsety, pw, ph = self._get_touch_resolution()
        pos = [x * pw + offsetx, y * ph + offsety]
        return pos

    def _get_touch_resolution(self):
        """
        获取设备的实际分辨率
        关键修改:使用 self.device 而不是 current_device()
        """
        if device_platform(self.device) == 'Android':
            if self.use_render_resolution:
                if self.render_resolution and len(self.render_resolution) == 4:
                    return self.render_resolution
                else:
                    return self.device.get_render_resolution()
        w, h = self.device.get_current_resolution()
        return 0, 0, w, h

    def setTouchDownDuration(self, duration):
        """设置触摸按下持续时间"""
        self.default_touch_down_duration = duration

    def getTouchDownDuration(self):
        """获取触摸按下持续时间"""
        return self.default_touch_down_duration

    def click(self, x, y):
        """点击操作 - 直接使用设备实例而不是全局 API"""
        pos = self.get_target_pos(x, y)
        # 直接调用设备的 touch 方法,不使用全局 API
        self.device.touch(pos, duration=self.default_touch_down_duration)

    def double_click(self, x, y):
        """双击操作 - 直接使用设备实例而不是全局 API"""
        pos = self.get_target_pos(x, y)
        # 直接调用设备的 double_click 方法
        self.device.double_click(pos)

    def swipe(self, x1, y1, x2, y2, duration=2.0):
        """滑动操作 - 直接使用设备实例而不是全局 API"""
        if duration <= 0:
            raise ValueError("Operation duration cannot be less equal 0. Please provide a positive number.")
        # Android.swipe() 接收起点和终点坐标,不接受 vector 参数
        # poco 传入的 x1,y1,x2,y2 是归一化坐标(0~1),需要转换为实际像素坐标
        p1 = self.get_target_pos(x1, y1)
        p2 = self.get_target_pos(x2, y2)
        steps = int(duration * 40) + 1
        self.device.swipe(p1, p2, duration=duration, steps=steps)

    def longClick(self, x, y, duration=2.0):
        """长按操作 - 直接使用设备实例而不是全局 API"""
        if duration <= 0:
            raise ValueError("Operation duration cannot be less equal 0. Please provide a positive number.")
        pos = self.get_target_pos(x, y)
        # 直接调用设备的 touch 方法,使用较长的 duration
        self.device.touch(pos, duration=duration)

    def applyMotionEvents(self, events):
        """
        应用运动事件序列
        关键修改:使用 self.device 而不是 current_device()
        """
        if device_platform(self.device) != 'Android':
            raise NotImplementedError

        # Android minitouch/maxtouch only, currently
        from airtest.core.android.touch_methods.base_touch import DownEvent, MoveEvent, UpEvent, SleepEvent

        mes = []
        for e in events:
            t = e[0]
            if t == 'd':
                contact = e[2]
                x, y = e[1]
                pos = self.get_target_pos(x, y)
                me = DownEvent(pos, contact)
            elif t == 'm':
                contact = e[2]
                x, y = e[1]
                pos = self.get_target_pos(x, y)
                me = MoveEvent(pos, contact)
            elif t == 'u':
                contact = e[1]
                me = UpEvent(contact)
            elif t == 's':
                how_long = e[1]
                me = SleepEvent(how_long)
            else:
                raise ValueError('Unknown event type {}'.format(repr(t)))
            mes.append(me)

        self.device.touch_proxy.perform(mes, interval=0)


class SafeStdPocoAgent(PocoAgent):
    """
    线程安全的 StdPocoAgent 实现
    使用指定的设备实例
    """
    def __init__(self, addr, device, use_airtest_input=True):
        self.conn = TcpClient(addr)
        self.c = RpcClient(self.conn)
        self.c.DEBUG = False
        self.c.connect()

        hierarchy = FrozenUIHierarchy(StdDumper(self.c), StdAttributor(self.c))
        screen = StdScreen(self.c)
        
        # 关键修改:使用 SafeAirtestInput 并传入设备实例
        if use_airtest_input:
            inputs = SafeAirtestInput(device)
        else:
            from poco.drivers.std.inputs import StdInput
            inputs = StdInput(self.c)
            
        super(SafeStdPocoAgent, self).__init__(hierarchy, inputs, screen, None)

    @property
    def rpc(self):
        return self.c


class SafeUnityPoco(StdPoco):
    """
    线程安全的 UnityPoco 实现
    
    主要改进:
    1. 不使用全局的 G.device
    2. 所有操作都使用传入的 device 参数
    3. 适用于多线程环境,每个线程可以有自己的设备实例
    
    Args:
        device: Android 设备实例,必须提供
        addr: Unity3D 游戏的端点地址,默认为 ("localhost", 5001)
        options: 其他 Poco 选项
    
    Examples:
        from airtest.core.android import Android
        from terot_poco_ui.util.PrivateUnityPoco import SafeUnityPoco
        
        # 创建设备实例(不使用 connect_device)
        dev = Android("ef4bdc41", host=("10.20.244.218", 5039))
        
        # 创建 poco 实例,绑定到指定设备
        poco = SafeUnityPoco(device=dev)
        
        # 正常使用
        btn = poco("m_Button_Close")
        btn.click()
    """
    
    DEFAULT_PORT = 5001
    DEFAULT_ADDR = ("localhost", DEFAULT_PORT)
    
    def __init__(self, device, addr=None, **options):
        if addr is None:
            addr = self.DEFAULT_ADDR
            
        if 'action_interval' not in options:
            options['action_interval'] = 0.5
        
        if device is None:
            raise ValueError("device parameter is required for SafeUnityPoco")
        
        self.device = device
        
        # 获取设备平台
        platform_name = device_platform(self.device)
        
        # 处理端口转发
        ip = addr[0]
        port = addr[1]
        
        if platform_name == 'Android':
            # Android 设备需要端口转发
            local_port, _ = self.device.adb.setup_forward('tcp:{}'.format(port))
            ip = self.device.adb.host or 'localhost'
            port = local_port
        elif platform_name == 'IOS':
            # iOS 设备端口转发
            port, _ = self.device.setup_forward(port)
            if self.device.is_local_device:
                ip = 'localhost'
            else:
                ip = self.device.ip
        else:
            # 其他平台尝试获取 IP
            try:
                ip = self.device.get_ip_address()
            except AttributeError:
                import socket
                try:
                    ip = socket.gethostbyname(socket.gethostname())
                except socket.gaierror:
                    ip = 'localhost'
        
        # 创建线程安全的 agent
        agent = SafeStdPocoAgent((ip, port), self.device, use_airtest_input=True)
        
        # 初始化父类
        options['reevaluate_volatile_attributes'] = True
        # 注意:这里直接调用 Poco 的初始化,跳过 StdPoco 的 __init__
        from poco.pocofw import Poco
        Poco.__init__(self, agent, **options)
    
    def send_message(self, message):
        """发送消息到 Unity"""
        self.agent.rpc.call("SendMessage", message)

    def invoke(self, listener, **kwargs):
        """调用 Unity 方法"""
        callback = self.agent.rpc.call("Invoke", listener=listener, data=kwargs)
        value, error = callback.wait()
        if error is not None:
            raise Exception(error)
        return value
AndroidPoco
# -*- coding: utf-8 -*-
# @Time    : 2026/4/23 17:35
# @Author  : Liam Li
# @FileName: PrivateAndroidPoco.py
# @Software: PyCharm
# @Description  :线程安全的 AndroidUiautomationPoco 实现,不依赖全局 G.device

import os
import requests
import time
import warnings
import threading
import atexit

from airtest.core.android.ime import YosemiteIme
from airtest.core.error import AdbShellError, AirtestError
from airtest.core.helper import device_platform, logwrap
from functools import wraps

from hrpc.client import RpcClient
from hrpc.transport.http import HttpTransport
from poco.pocofw import Poco
from poco.agent import PocoAgent
from poco.sdk.Attributor import Attributor
from poco.sdk.interfaces.screen import ScreenInterface
from poco.sdk.interfaces.input import InputInterface
from poco.utils.hrpc.hierarchy import RemotePocoHierarchy
from poco.utils import six
from poco.drivers.android.utils.installation import install, uninstall

__all__ = ['SafeAndroidUiautomationPoco']

# 包名常量
PocoServicePackage = 'com.netease.open.pocoservice'
PocoServicePackageTest = 'com.netease.open.pocoservice.test'
UiAutomatorPackage = 'com.github.uiautomator'


def serializable_adapter(func):
    """序列化适配器装饰器"""
    class ISerializable(object):
        def to_json(self):
            raise NotImplementedError

    @wraps(func)
    def wrapped(*args):
        class PocoUIProxySerializable(ISerializable):
            def __init__(self, obj):
                self.obj = obj

            def to_json(self):
                return repr(self.obj)

        new_args = [PocoUIProxySerializable(a) for a in args]
        return func(*new_args)

    return wrapped


@serializable_adapter
@logwrap
def record_ui(driver, action, ui, args):
    """记录 UI 操作"""
    return ui


class SafeAirtestInput(InputInterface):
    """
    线程安全的 AirtestInput 实现
    使用指定的设备实例而不是全局 current_device()
    """
    def __init__(self, device):
        super(SafeAirtestInput, self).__init__()
        self.device = device  # 使用传入的设备实例
        self.default_touch_down_duration = 0.01
        self._driver = None
        self.use_render_resolution = False
        self.render_resolution = None

    def add_preaction_cb(self, driver):
        """添加预操作回调"""
        self._driver = driver
        self._driver.add_pre_action_callback(record_ui)

    def get_target_pos(self, x, y):
        """获取目标位置的实际坐标"""
        offsetx, offsety, pw, ph = self._get_touch_resolution()
        pos = [x * pw + offsetx, y * ph + offsety]
        return pos

    def _get_touch_resolution(self):
        """
        获取设备的实际分辨率
        关键修改:使用 self.device 而不是 current_device()
        """
        if device_platform(self.device) == 'Android':
            if self.use_render_resolution:
                if self.render_resolution and len(self.render_resolution) == 4:
                    return self.render_resolution
                else:
                    return self.device.get_render_resolution()
        w, h = self.device.get_current_resolution()
        return 0, 0, w, h

    def setTouchDownDuration(self, duration):
        """设置触摸按下持续时间"""
        self.default_touch_down_duration = duration

    def getTouchDownDuration(self):
        """获取触摸按下持续时间"""
        return self.default_touch_down_duration

    def click(self, x, y):
        """点击操作 - 直接使用设备实例而不是全局 API"""
        pos = self.get_target_pos(x, y)
        # 直接调用设备的 touch 方法,不使用全局 API
        self.device.touch(pos, duration=self.default_touch_down_duration)

    def double_click(self, x, y):
        """双击操作 - 直接使用设备实例而不是全局 API"""
        pos = self.get_target_pos(x, y)
        # 直接调用设备的 double_click 方法
        self.device.double_click(pos)

    def swipe(self, x1, y1, x2, y2, duration=2.0):
        """滑动操作 - 直接使用设备实例而不是全局 API"""
        if duration <= 0:
            raise ValueError("Operation duration cannot be less equal 0. Please provide a positive number.")
        # Android.swipe() 接收起点和终点坐标,不接受 vector 参数
        # poco 传入的 x1,y1,x2,y2 是归一化坐标(0~1),需要转换为实际像素坐标
        p1 = self.get_target_pos(x1, y1)
        p2 = self.get_target_pos(x2, y2)
        steps = int(duration * 40) + 1
        self.device.swipe(p1, p2, duration=duration, steps=steps)

    def longClick(self, x, y, duration=2.0):
        """长按操作 - 直接使用设备实例而不是全局 API"""
        if duration <= 0:
            raise ValueError("Operation duration cannot be less equal 0. Please provide a positive number.")
        pos = self.get_target_pos(x, y)
        # 直接调用设备的 touch 方法,使用较长的 duration
        self.device.touch(pos, duration=duration)

    def applyMotionEvents(self, events):
        """
        应用运动事件序列
        关键修改:使用 self.device 而不是 current_device()
        """
        if device_platform(self.device) != 'Android':
            raise NotImplementedError

        # Android minitouch/maxtouch only, currently
        from airtest.core.android.touch_methods.base_touch import DownEvent, MoveEvent, UpEvent, SleepEvent

        mes = []
        for e in events:
            t = e[0]
            if t == 'd':
                contact = e[2]
                x, y = e[1]
                pos = self.get_target_pos(x, y)
                me = DownEvent(pos, contact)
            elif t == 'm':
                contact = e[2]
                x, y = e[1]
                pos = self.get_target_pos(x, y)
                me = MoveEvent(pos, contact)
            elif t == 'u':
                contact = e[1]
                me = UpEvent(contact)
            elif t == 's':
                how_long = e[1]
                me = SleepEvent(how_long)
            else:
                raise ValueError('Unknown event type {}'.format(repr(t)))
            mes.append(me)

        self.device.touch_proxy.perform(mes, interval=0)


class AndroidRpcClient(RpcClient):
    """Android RPC 客户端"""
    def __init__(self, endpoint):
        self.endpoint = endpoint
        super(AndroidRpcClient, self).__init__(HttpTransport)

    def initialize_transport(self):
        return HttpTransport(self.endpoint, self)


class ScreenWrapper(ScreenInterface):
    """屏幕接口包装器"""
    def __init__(self, screen):
        super(ScreenWrapper, self).__init__()
        self.screen = screen

    def getScreen(self, width):
        # Android上PocoService的实现为仅返回b64编码的图像,格式固定位jpg
        b64img = self.screen.getScreen(width)
        return b64img, 'jpg'

    def getPortSize(self):
        return self.screen.getPortSize()


class SafeAndroidPocoAgent(PocoAgent):
    """
    线程安全的 AndroidPocoAgent 实现
    使用指定的设备实例
    """
    def __init__(self, endpoint, ime, device, use_airtest_input=False):
        self.client = AndroidRpcClient(endpoint)
        remote_poco = self.client.remote('poco-uiautomation-framework')
        dumper = remote_poco.dumper
        selector = remote_poco.selector
        attributor = remote_poco.attributor
        hierarchy = RemotePocoHierarchy(dumper, selector, attributor)

        # 关键修改:使用 SafeAirtestInput 并传入设备实例
        if use_airtest_input:
            inputer = SafeAirtestInput(device)
        else:
            inputer = remote_poco.inputer
            
        super(SafeAndroidPocoAgent, self).__init__(hierarchy, inputer, ScreenWrapper(remote_poco.screen), None)


class KeepRunningInstrumentationThread(threading.Thread):
    """保持 pocoservice 运行的后台线程"""

    def __init__(self, poco, port_to_ping):
        super(KeepRunningInstrumentationThread, self).__init__()
        self._stop_event = threading.Event()
        self.poco = poco
        self.port_to_ping = port_to_ping
        self.daemon = True

    def stop(self):
        self._stop_event.set()

    def stopped(self):
        return self._stop_event.is_set()

    def run(self):
        while not self.stopped():
            if not self.stopped():
                self.poco._start_instrument(self.port_to_ping)  # 尝试重启
                time.sleep(1)


class SafeAndroidUiautomationPoco(Poco):
    """
    线程安全的 AndroidUiautomationPoco 实现
    
    主要改进:
    1. 不使用全局的 G.device 和 default_device()
    2. 所有操作都使用传入的 device 参数
    3. 适用于多线程环境,每个线程可以有自己的设备实例
    
    Args:
        device: Android 设备实例,必须提供
        using_proxy: 是否使用 adb forward 连接设备,默认 True
        force_restart: 是否强制重启 poco-service,默认 False
        use_airtest_input: 是否使用 airtest 的输入方法,默认 False
        options: 其他 Poco 选项
    
    Examples:
        from airtest.core.android import Android
        from terot_poco_ui.util.PrivateAndroidPoco import SafeAndroidUiautomationPoco
        
        # 创建设备实例(不使用 connect_device)
        dev = Android("ef4bdc41", host=("10.20.244.218", 5039))
        
        # 创建 poco 实例,绑定到指定设备
        poco = SafeAndroidUiautomationPoco(device=dev)
        
        # 正常使用
        poco('android:id/title').click()
    """

    def __init__(self, device, using_proxy=True, force_restart=False, use_airtest_input=False, **options):
        # 加这个参数为了不在最新的pocounit方案中每步都截图
        self.screenshot_each_action = True
        if options.get('screenshot_each_action') is False:
            self.screenshot_each_action = False

        # 关键修改:强制要求 device 参数
        if device is None:
            raise ValueError("device parameter is required for SafeAndroidUiautomationPoco")
        
        self.device = device

        self.adb_client = self.device.adb
        if using_proxy:
            self.device_ip = self.adb_client.host or "127.0.0.1"
        else:
            self.device_ip = self.device.get_ip_address()

        # save current top activity (@nullable)
        try:
            current_top_activity_package = self.device.get_top_activity_name()
        except AirtestError as e:
            # 在一些极端情况下,可能获取不到top activity的信息
            print(e)
            current_top_activity_package = None
        if current_top_activity_package is not None:
            current_top_activity_package = current_top_activity_package.split('/')[0]

        # install ime
        self.ime = YosemiteIme(self.adb_client)

        # install
        this_dir = os.path.dirname(os.path.realpath(__file__))
        # 注意:这里需要使用原生的 poco 库路径
        poco_lib_dir = os.path.dirname(os.path.dirname(os.path.dirname(
            os.path.dirname(os.path.realpath(__file__)))))
        apk_path = os.path.join(poco_lib_dir, '.venv', 'lib', 'site-packages', 
                                'poco', 'drivers', 'android', 'lib', 'pocoservice-debug.apk')
        
        # 如果找不到,尝试从当前环境查找
        if not os.path.exists(apk_path):
            import poco.drivers.android
            android_driver_dir = os.path.dirname(poco.drivers.android.__file__)
            apk_path = os.path.join(android_driver_dir, 'lib', 'pocoservice-debug.apk')
        
        self._instrument_proc = None
        self._install_service(apk_path)

        # forward
        self.forward_list = []
        if using_proxy:
            p0, _ = self.adb_client.setup_forward("tcp:10080")
            p1, _ = self.adb_client.setup_forward("tcp:10081")
            self.forward_list.extend(["tcp:%s" % p0, "tcp:%s" % p1])
        else:
            p0 = 10080
            p1 = 10081

        # start
        ready = self._start_instrument(p0, force_restart=force_restart)
        if not ready:
            # 之前启动失败就卸载重装,现在改为尝试kill进程或卸载uiautomator
            self._kill_uiautomator()
            ready = self._start_instrument(p0)

            if current_top_activity_package is not None:
                current_top_activity2 = self.device.get_top_activity_name()
                if current_top_activity2 is None or current_top_activity_package not in current_top_activity2:
                    self.device.start_app(current_top_activity_package, activity=True)

            if not ready:
                raise RuntimeError("unable to launch SafeAndroidUiautomationPoco")
        if ready:
            # 首次启动成功后,在后台线程里监控这个进程的状态,保持让它不退出
            self._keep_running_thread = KeepRunningInstrumentationThread(self, p0)
            self._keep_running_thread.start()

        endpoint = "http://{}:{}".format(self.device_ip, p1)
        # 关键修改:创建 SafeAndroidPocoAgent 并传入设备实例
        agent = SafeAndroidPocoAgent(endpoint, self.ime, self.device, use_airtest_input)
        super(SafeAndroidUiautomationPoco, self).__init__(agent, **options)

    def _install_service(self, apk_path):
        """安装 poco service"""
        updated = install(self.adb_client, apk_path)
        return updated

    def _is_running(self, package_name):
        """
        使用 ps |grep 检查进程是否存在

        :param package_name: 包名或正则表达式
        :return: pid or None
        """
        cmd = r' |echo $(grep -E {package_name})'.format(package_name=package_name)
        if self.device.sdk_version > 25:
            cmd = r'ps -A' + cmd
        else:
            cmd = r'ps' + cmd
        processes = self.adb_client.shell(cmd).splitlines()
        for ps in processes:
            if ps:
                ps = ps.split()
                return ps[1]
        return None

    def _start_instrument(self, port_to_ping, force_restart=False):
        """启动 instrumentation"""
        if not force_restart:
            try:
                state = requests.get('http://{}:{}/uiautomation/connectionState'.format(self.device_ip, port_to_ping),
                                     timeout=10)
                state = state.json()
                if state.get('connected'):
                    # skip starting instrumentation if UiAutomation Service already connected.
                    return True
            except:
                pass

        if self._instrument_proc is not None:
            if self._instrument_proc.poll() is None:
                self._instrument_proc.kill()
            self._instrument_proc = None

        ready = False

        # 启动instrument之前,先把主类activity启动起来,不然instrumentation可能失败
        self.adb_client.shell('am start -n {}/.TestActivity'.format(PocoServicePackage))

        instrumentation_cmd = [
            'am', 'instrument', '-w', '-e', 'debug', 'false', '-e', 'class',
            '{}.InstrumentedTestAsLauncher'.format(PocoServicePackage),
            '{}/androidx.test.runner.AndroidJUnitRunner'.format(PocoServicePackage)]
        self._instrument_proc = self.adb_client.start_shell(instrumentation_cmd)

        def cleanup_proc(proc):
            def wrapped():
                try:
                    proc.kill()
                except:
                    pass
            return wrapped
        atexit.register(cleanup_proc(self._instrument_proc))

        time.sleep(2)
        for i in range(10):
            try:
                requests.get('http://{}:{}'.format(self.device_ip, port_to_ping), timeout=10)
                ready = True
                break
            except requests.exceptions.Timeout:
                break
            except requests.exceptions.ConnectionError:
                if self._instrument_proc.poll() is not None:
                    warnings.warn("[pocoservice.apk] instrumentation test server process is no longer alive")
                    stdout = self._instrument_proc.stdout.read()
                    stderr = self._instrument_proc.stderr.read()
                    print('[pocoservice.apk] stdout: {}'.format(stdout))
                    print('[pocoservice.apk] stderr: {}'.format(stderr))
                time.sleep(1)
                print("still waiting for uiautomation ready.")
                try:
                    self.adb_client.shell(
                        ['monkey', '-p', PocoServicePackage, '-c', 'android.intent.category.LAUNCHER', '1'])
                except Exception as e:
                    pass
                self.adb_client.shell('am start -n {}/.TestActivity'.format(PocoServicePackage))
                instrumentation_cmd = [
                    'am', 'instrument', '-w', '-e', 'debug', 'false', '-e', 'class',
                    '{}.InstrumentedTestAsLauncher'.format(PocoServicePackage),
                    '{}/androidx.test.runner.AndroidJUnitRunner'.format(PocoServicePackage)]
                self._instrument_proc = self.adb_client.start_shell(instrumentation_cmd)
                continue
        return ready

    def _kill_uiautomator(self):
        """
        poco-service无法与其他instrument启动的apk同时存在,因此在启动前,需要杀掉一些可能的进程
        """
        pid = self._is_running("uiautomator")
        if pid:
            warnings.warn('{} should not run together with "uiautomator". "uiautomator" will be killed.'
                          .format(self.__class__.__name__))
            self.adb_client.shell(['am', 'force-stop', PocoServicePackage])

            try:
                self.adb_client.shell(['kill', pid])
            except AdbShellError:
                # 没有root权限
                uninstall(self.adb_client, UiAutomatorPackage)

    def on_pre_action(self, action, ui, args):
        """操作前的回调"""
        if self.screenshot_each_action:
            # airtest log用
            from airtest.core.api import snapshot
            msg = repr(ui)
            if not isinstance(msg, six.text_type):
                msg = msg.decode('utf-8')
            snapshot(msg=msg)

    def stop_running(self):
        """停止运行 PocoService"""
        print('[pocoservice.apk] stopping PocoService')
        self._keep_running_thread.stop()
        self._keep_running_thread.join(3)
        self.remove_forwards()
        self.adb_client.shell(['am', 'force-stop', PocoServicePackage])

    def remove_forwards(self):
        """移除端口转发"""
        for p in self.forward_list:
            self.adb_client.remove_forward(p)
        self.forward_list = []

使用方式

from PrivateUnityPoco import SafeUnityPoco as UnityPoco
from PrivateAndroidPoco import SafeAndroidUiautomationPoco as AndroidPoco

dev = Android("ef334bdc41", host=("101.220.244.218", 5039))

# 创建 UnityPoco实例,绑定到指定设备
poco = UnityPoco(device=dev)

# 创建 AndroidPoco实例,绑定到指定设备
poco = AndroidPoco(device=dev)

 

posted @ 2026-05-09 14:57  ShineLeem  阅读(8)  评论(0)    收藏  举报