通用安卓弹窗点击方法
封装的方法
1 import contextlib 2 import logging 3 import re 4 import threading 5 import time 6 from dataclasses import dataclass 7 from airtest.core.android import Android 8 from airtest.core.ios import IOS 10 11 logger = logging.getLogger(__name__) 12 20 21 class DeviceSession: 22 """ 23 线程内设备会话: 24 - dev 固定 26 """ 27 def __init__(self, host: str, serialno: str, adb_port: int = 5039, 28 policy: PocoBuildPolicy | None = None): 29 self.host = host 30 self.serialno = serialno 32 self._lock = threading.RLock() 33 34 # 远程测试农场用 5039,本地 USB 设备用标准端口 5037 35 self.dev = Android(serialno=self.serialno, host=(self.host, adb_port)) 37 self._generation = 0 # 游戏重启后 +1,触发重建 3 92 93 @contextlib.contextmanager 94 def alert_watch_and_click(self, buttons: list, interval: float = 3.0): 95 """ 96 后台监控 Android 系统弹窗并自动点击,用法与 iOS 的 wda alert_watch_and_click 一致。 97 98 原理: 99 启动一个 daemon 后台线程,每隔 interval 秒用 adb shell 执行 uiautomator dump, 100 解析当前界面上的可点击文字,如果匹配到 buttons 中的任意一个就点击它。 101 102 Args: 103 buttons: 需要点击的按钮文字列表,按优先级排列,匹配到第一个就点击 104 例如:["允许", "同意", "确定", "好"] 105 interval: 检查间隔(秒),默认 3 秒 106 107 Examples: 108 with self.device_init.session.alert_watch_and_click(["允许", "同意", "确定"], interval=3): 109 self.device_init.dev.adb.shell("pm install xxx.apk") 110 time.sleep(30) 111 """ 112 stop_event = threading.Event() 113 114 def _watch_worker(): 115 while not stop_event.is_set(): 116 try: 117 self._check_and_click_alert(buttons) 118 except Exception as e: 119 logger.debug(f"[alert_watch] 检查弹窗异常(可忽略): {e}") 120 # 用 wait 代替 sleep,这样 stop_event 被 set 后能立即退出,不用等满 interval 121 stop_event.wait(interval) 122 123 t = threading.Thread(target=_watch_worker, name="android-alert-watcher", daemon=True) 124 t.start() 125 logger.info(f"[alert_watch] 开始监控弹窗,目标按钮: {buttons},间隔: {interval}s") 126 try: 127 yield 128 finally: 129 stop_event.set() 130 t.join(timeout=interval + 1) 131 logger.info("[alert_watch] 弹窗监控已停止") 132 133 def _dump_ui_xml(self, max_retry: int = 3) -> str: 134 """ 135 dump 当前界面层级并返回完整 XML,失败返回空串。 136 137 内存紧张时(前台游戏占满内存),uiautomator dump 新起的进程会被系统 OOM 杀掉 138 (回显为 "Killed"),文件写不出来。这里做两件事缓解: 139 1. --compressed:只保留对无障碍有意义的节点,层级树更小、占内存更少,弹窗按钮通常会保留。 140 2. 单次检测内重试若干次:OOM 多为瞬时,重试期间内存可能已回收。 141 判据:cat 出的内容必须是含结尾 </hierarchy> 的完整 XML(前面已 rm,杜绝读到旧残留)。 142 """ 143 dump_out = "" 144 for i in range(1, max_retry + 1): 145 # 先删旧文件,避免本次失败时 cat 到上一次成功的残留 XML(明明有弹窗却读到旧界面)。 146 self.dev.adb.shell("rm -f /sdcard/alert_dump.xml") 147 # 成功/失败提示都走 stderr,用 2>&1 重定向回 stdout 以便记录真正原因。 148 dump_out = self.dev.adb.shell("uiautomator dump --compressed /sdcard/alert_dump.xml 2>&1") or "" 149 xml_content = self.dev.adb.shell("cat /sdcard/alert_dump.xml") or "" 150 if "<?xml" in xml_content and "</hierarchy>" in xml_content: 151 return xml_content 152 if i < max_retry: 153 time.sleep(0.5) 154 logger.debug(f"[alert_watch] dump 连续 {max_retry} 次失败,跳过本次检测,最后一次 dump 输出: {dump_out.strip()!r}") 155 return "" 156 157 def _check_and_click_alert(self, buttons: list): 158 """ 159 执行一次弹窗检测和点击。 160 dump UI 只做一次,避免在 _watch_worker 里对每个按钮文字各 dump 一次, 161 造成多次 adb 调用和 XML 不一致的问题。 162 """ 163 xml_content = self._dump_ui_xml() 164 if not xml_content: 165 return 166 for btn_text in buttons: 167 # 匹配完整的 <node .../> 标签,同时兼容 text 和 content-desc 属性 168 # 前瞻断言确认节点包含目标文字后,再匹配完整标签,避免跨节点误匹配 bounds 169 node_match = re.search( 170 rf'<node(?=[^>]*(?:text|content-desc)="{re.escape(btn_text)}")[^>]*/>', 171 xml_content 172 ) 173 if not node_match: 174 continue 175 node = node_match.group() 176 177 # 复选框已勾选时跳过,不重复点击 178 checkable = 'checkable="true"' in node 179 checked = 'checked="true"' in node 180 if checkable and checked: 181 logger.debug(f"[alert_watch] '{btn_text}' 复选框已勾选,跳过") 182 continue 183 184 # 从匹配到的 node 串中提取 bounds 185 bounds_match = re.search(r'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', node) 186 if not bounds_match: 187 continue 188 x1, y1, x2, y2 = (int(bounds_match.group(1)), int(bounds_match.group(2)), 189 int(bounds_match.group(3)), int(bounds_match.group(4))) 190 cx = (x1 + x2) // 2 191 cy = (y1 + y2) // 2 192 logger.info(f"[alert_watch] 点击 '{btn_text}' checkable={checkable} checked={checked},坐标: ({cx}, {cy})") 193 self.dev.adb.shell(f"input tap {cx} {cy}") 194 # 每轮只处理一个,避免误点多个按钮 195 return 196 197 logger.debug(f"[alert_watch] 未检测到目标弹窗按钮: {buttons}")
使用方法
import time from DeviceSession import DeviceSession session = DeviceSession(host="127.0.0.1", serialno="10.30.244.58:20012", adb_port=5037) with session.alert_watch_and_click([ "已了解应用的风险检测结果", "继续安装", "安装", "稍后", "稍后提醒", "允许", "始终允许", "同意", "同意并继续", "传输文件", "传输文件(MTP)", "忽略本次", "登录", "完成", "退出" ], interval=3): time.sleep(200) # 放入装包的命令 time.sleep(3) print("继续开始检测")

浙公网安备 33010602011771号