Files
auto_control/doc/research/U2_ELEMENT_SELECTORS.md
T
butubb 37a2b1c59b feat(抓取): 元素检查器改成本地版"大图 + 一次取齐"——复刻云检查器的体验
用户要的是 uiauto.dev 那个检查器的体验(左边大图点元素、右边层级树、选完回填),
并希望本地复刻(云页面跨域,拿不到它的选中结果,没法自动回传)。

- core/uiauto_helper.py: 新增 `snapshot(serial)`
  * **一个 u2 连接背靠背** dump_hierarchy() + screenshot():截图与元素树同源同刻,
    不再像原来那样分两个接口取(中间隔着 dump 本身的 1.3~1.8 秒)
  * `_xml_to_node()`:把 u2 的 XML 节点转成 uiautodev 那套结构,直接复用既有的
    选择器建议逻辑(不写第二遍)
  * **双截图校验**:dump 前后各截一张,差异明显就标 `unstable`,让前端明确提示
    "界面在变化中,请停在静止界面再抓",而不是悄悄给一个可能错位的框
- web/tasks_api.py: 新增 `GET /api/uiauto/snapshot`(登录 + 设备权限)
- static/admin/editor.js + templates/admin/monitor.html:
  * 抓取弹窗从 900px 加宽到 1280px,预览列从"固定 320px"改为铺满左侧 →
    **图片按原始分辨率 1:1 显示**(实测 720x1650 缩放 1.000),元素框严丝合缝;
    原来缩到 294px 时框全挤在一起,看着就像错位
  * 工具栏显示 `720x1650 · 247 个元素 · 2370ms`;界面在变时顶部弹黄色提示条
  * 鼠标在图上移动时高亮"最深命中"的元素框,便于确认真要点哪个

实测:抓取弹窗 1:1 显示、222 个框逐一贴合元素、无 JS 报错;
`unstable` 在静止界面为 false。
2026-09-13 21:17:12 +08:00

6.6 KiB
Raw Blame History

元素选择器研究:为什么"点不到按钮"(2026-09-13)

背景:任务步骤(click 等)频繁出现"元素明明在屏幕上却点不到"。用户怀疑是 任务执行用的 u2 与元素抓取用的 uiautodev 两条通道不一致,要求研究。 分支:research/raw-u2-elements。


一、先否掉一个假设:两条通道并不冲突

用的东西 连接方式
任务执行(点击/输入…) 原生 u2.connect(serial) 直连设备的 uiautomator2 server
元素抓取(编辑器按钮) uiautodev 服务(localhost:20242)→ /api/android/{serial}/hierarchy 它自己的连接

实测(同一设备、同一屏、同一时刻,两边各 dump 一次):

对比项 u2 uiautodev
节点总数 330 330
id/0qf(抖音底部 tab) 3 3
id/content_layout 5 5

逐项一致 —— 两个通道给出的元素树是同一份(都是设备的 UiAutomation dump)。 所以"抓取看到的和执行看到的不是一回事"这个假设不成立。

顺带纠正一处过时注释:core/uiauto_helper.py 说"uiautodev 归一化浮点坐标无法换算像素"。 实际上 uiautodev 的节点里 rect 就是像素({x,y,width,height}),bounds 才是 归一化的 0~1 浮点。要用像素直接用 rect 即可。


二、真正的原因:序号型选择器((…)[k])会随界面变形而错位

复现(192.168.20.100,抖音 v40.4.0)—— 进「我」页面,底部导航 dump 出来是:

[1] text='首页'  bounds=[38,1484][106,1530]
[2] text='消息'  bounds=[470,1484][538,1530]
[3] text='我'    bounds=[631,1484][665,1530]

只有 3 个 0qf。而任务里的步骤写的是:

(//*[@resource-id="com.ss.android.ugc.aweme:id/0qf"])[4]    ← 第 4 个,根本不存在

必然点空 —— 日志里表现为 click 未找到元素,而界面上那个「我」明明就在那儿。

为什么有的设备是 4 个?

抖音底部的「朋友」tab 是灰度功能:有的账号/设备有(首页/朋友/消息/我 = 4 个), 有的没有(首页/消息/我 = 3 个)。同一个序号 [4]:

  • 4 个 tab 的设备 → 点到「我」(碰巧对)
  • 3 个 tab 的设备 → 点空(或点到别的东西)

同一个选择器在不同机器上语义不同,这是"时好时坏"的根源。


三、解法:用语义选择器,不要依赖序号

实测有效(同一台 3-tab 设备):

# ✗ 序号型:依赖"底部有几个 tab"
(//*[@resource-id="com.ss.android.ugc.aweme:id/0qf"])[4]

# ✓ 语义型:同一个 id,用文字限定 —— 不管有几个 tab 都对
//*[@resource-id="com.ss.android.ugc.aweme:id/0qf" and @text="我"]

验证:命中(bounds 落在「我」上)→ 点击 → 进入我页面 → jy-(切换按钮)出现 ✓

同类可用限定条件(按优先级):

  1. @text="…" —— 文字最稳(tab 名、按钮名)
  2. @content-desc="…" —— 无文字但有描述(如 首页,按钮)
  3. @resource-id="…" —— 唯一时直接用
  4. 组合://*[@resource-id="x" and @text="y"] —— 同一 id 多实例时的最佳解
  5. 结构路径(…/FrameLayout[2])—— 最后手段,最脆
  6. (…)[k] —— 只在上面都不行时

四、抓取弹窗"老是错位"的根因 —— 截图和元素树不是同一时刻

(2026-09-13 追加,用户反馈"抓取的窗口老是会错位")

先把几个常见猜测逐一排除(都实测过):

猜测 实测结果
两条通道元素树不一致 ✗ 排除:节点数/各 id 个数逐项相同
截图被 CSS 缩放了、框没跟着缩 ✗ 排除:scaleX=clientWidth/naturalWidth 算得对,实测框位置与理论值一致
窗口 resize 后没重算 ✗ 排除:预览列固定 320px,resize 不影响
列表索引与框的 data-idx 对不上 ✗ 排除:用 indexOf 保住原始序号,过滤后也不乱

真因:截图与元素树分两次取,中间隔了 1~2 秒。

取数方式 截图 元素树 两者时间差
现在(/api/uiauto/screenshot + /api/uiauto/elements 两次请求,走 uiautodev) 0.4s 1.8s 约 1.8 秒
原生 u2 背靠背(同一个连接) 0.3s 1.4s 约 1.4 秒

元素树的 dump 本身就要 1.3~1.8 秒(设备端 uiautomator 的开销,改不动)。 界面只要在动(抖音信息流、视频、加载动画…),两秒足够让元素位置全变 → 框永远落在旧位置上 —— 这就是"老是错位",不是偶发。

解法(✅ 2026-09-13 已实现,见 core/uiauto_helper.snapshot)

  1. 一次请求取齐(推荐,也是用户说的"用原始 u2"): 新增 GET /api/uiauto/snapshot?serial=X,服务端用一个 u2 连接背靠背 dump_hierarchy() + screenshot(),返回 {image, elements}; 前端只调这一个接口 → 天然同源,间隔从 ~2.2s 降到 ~1.4s。 并且抓取弹窗改成大图 1:1 预览(原来预览列只有 320px、图片被缩到 294px, 框全挤在一起,看着就像"错位";现在铺满左侧、按原始分辨率显示)。
  2. 双截图校验(关键:让失败可见而不是悄悄错位): 抓取时前后各截一张,两张不一致就明确提示"界面在抓取过程中变化了, 请让设备停在目标界面再抓",而不是给一个已经错位的框。
  3. 文档里明确写:抓元素要让设备停在静止界面(设置页、已加载完的页面), 别在视频播放/信息流滑动中抓。

五、建议的改动(待排期)

  1. 抓取器(core/uiauto_helper.py):同一 id 出现多个实例时,优先产出 //*[@resource-id="x" and @text="…"] 而不是 (…)[k];抓取列表里把 text/content-desc 放在最显眼位置,让用户能看着选。
  2. 抓取弹窗:对带序号的选择器加醒目提示("这个选择器依赖界面元素个数, 界面一变就失效")。
  3. 已存任务:把 (…)[k] 型选择器扫一遍,能改成文字限定的自动改(脚本), 其余在编辑器里逐个复核。
  4. 运行期:click 未命中时,日志里顺带 dump 一下"同 id 现在有几个实例", 让"序号错位"当场可诊断(现在是干巴巴一句"未找到元素")。
  5. (针对第四节的"抓取错位") 抓取接口合并成一个快照接口 + 双截图校验, 具体见第四节「解法」。