资讯动态

手机GUI Agent双通道感知:无障碍树与投屏像素融合实践

发布时间:2026/10/26 16:53:02 来源:尧图企业网站定制
1. 为什么手机 GUI Agent 需要双通道感知做过手机端 GUI Agent 的人都有一个共同体会单靠一种感知方式几乎不可能把界面理解做稳。我最早做自动化操作的时候只依赖无障碍树AccessibilityService 提供的节点树跑通几个简单 App 之后信心满满结果一换到自绘 UI 比较重的应用上整棵树的节点信息几乎全是空的控件没有文本、没有 ID、层级还乱Agent 直接瞎了。后来我又试过纯靠投屏像素做视觉识别用 MediaProjection 抓屏再跑图像模型这条路在通用性上确实强但延迟高、算力贵而且很多纯文本按钮识别起来反而没有结构化信息来得准。踩了几轮坑之后我最终定下来的方案是双通道感知以无障碍树为主通道投屏像素作为兜底通道。核心逻辑很简单——能用结构化信息解决的就不要动用视觉视觉只在无障碍树失效或者信息不足的时候才介入。这样既保证了大部分场景下的低延迟和高准确率又能在极端情况下不至于完全瘫痪。这套方案适合谁如果你正在做手机端的自动化测试、RPA 流程、GUI Agent 交互或者任何需要让程序看懂手机屏幕的事情这套双通道思路都能直接参考。它不依赖特定框架核心是感知层的设计上层用什么决策模型都可以对接。下面我会把整套设计思路、关键实现细节、参数取舍和踩过的坑完整拆开讲。2. 双通道感知的整体架构设计2.1 主辅通道的职责划分双通道不是简单地两个都用而是有明确的主次和触发条件。我把职责划分成三层主通道无障碍树负责绝大多数常规界面的元素定位、文本提取、可点击区域判断、层级关系还原。只要无障碍树能给出足够信息就完全不启动像素通道。兜底通道投屏像素当无障碍树节点缺失、文本为空、或者 Agent 需要理解非标准控件比如游戏画面、Canvas 绘制、图片按钮时启动通过截图做视觉理解。融合层把两个通道的结果做对齐和互补比如用无障碍树给出的坐标框去裁剪像素图减少视觉模型的处理范围。这样设计的好处是成本可控。无障碍树的读取基本是毫秒级几乎不耗算力而投屏像素从抓帧到推理动辄几百毫秒甚至更久。如果每个操作都走视觉Agent 的响应速度会慢到没法用。2.2 为什么不让像素通道当主力有人会问既然视觉通用性最强为什么不干脆全用像素我实测下来的原因有三个。第一是延迟。MediaProjection 抓一帧加上编码传输再跑一次视觉推理整个链路在中等配置手机上轻松超过 500ms。而无障碍树一次遍历通常 20-50ms 就够。对于需要连续操作的任务这个差距会被放大到无法接受。第二是精度。视觉模型对文本的识别会有误差尤其是小字号、低对比度、特殊字体的情况。而无障碍树里的文本是系统直接给的零误差。像确认和确定这种一字之差视觉可能认错结构化信息不会。第三是成本。视觉推理要么占本地算力要么走网络请求两者都是实打实的开销。无障碍树是系统免费提供的不用白不用。所以主辅关系不能颠倒。像素通道是保险不是主力。2.3 通道切换的判定策略切换逻辑是整套方案的核心。我用的判定规则大致是这样判定条件处理方式无障碍树节点数 阈值 且 关键节点有文本只用主通道无障碍树节点数正常 但 目标区域无文本/无ID主通道定位 像素裁剪补充无障碍树节点数 阈值 或 根节点为空直接切像素通道目标为图片/Canvas/游戏画面直接切像素通道阈值我一般设在 5 左右也就是整棵树有效节点少于 5 个就认为无障碍信息不可靠。这个值不是拍脑袋定的是统计了几十个常见 App 的界面后取的经验值——正常界面少说也有十几个有效节点低于 5 个基本就是自绘 UI 了。注意判定策略不要写死最好做成可配置。不同 App 的无障碍实现质量差异很大有些 App 明明有节点但故意隐藏文本这种情况阈值判定会失效需要额外加白名单或黑名单机制。3. 无障碍树通道的核心实现细节3.1 AccessibilityService 的配置要点无障碍通道的基础是 AccessibilityService。配置本身不复杂但有几个坑必须提前避开。首先是accessibilityFlags的设置。我一般会加上FLAG_RETRIEVE_INTERACTIVE_WINDOWS这样才能拿到多个窗口的节点否则弹窗、悬浮窗里的内容读不到。另外FLAG_REPORT_VIEW_IDS也建议开方便拿到控件的资源 ID 做精确定位。accessibility-service android:accessibilityEventTypestypeAllMask android:accessibilityFeedbackTypefeedbackGeneric android:accessibilityFlagsflagRetrieveInteractiveWindows|flagReportViewIds android:canRetrieveWindowContenttrue android:notificationTimeout100 /notificationTimeout我设成 100ms太短会导致事件风暴太长会漏掉快速变化的界面。这个值可以根据实际场景调做静态页面分析可以设大一点做实时交互就得设小。3.2 节点树的遍历与信息提取拿到AccessibilityNodeInfo之后遍历方式直接决定了信息质量。我的做法是深度优先遍历同时提取这几类关键信息text和contentDescription文本内容两个都要看很多图标按钮的语义藏在 contentDescription 里。viewIdResourceName资源 ID最稳定的定位依据。boundsInScreen屏幕坐标用于和像素通道对齐。isClickable、isScrollable、isEditable交互属性判断元素能不能操作。className控件类型辅助判断。遍历的时候要注意回收节点。AccessibilityNodeInfo用完必须调recycle()否则在高频遍历场景下会内存泄漏。我早期就吃过这个亏跑久了直接 OOM。void traverse(AccessibilityNodeInfo node, ListNodeData result) { if (node null) return; NodeData data extract(node); if (data.isValid()) result.add(data); for (int i 0; i node.getChildCount(); i) { AccessibilityNodeInfo child node.getChild(i); traverse(child, result); if (child ! null) child.recycle(); } }3.3 节点信息的清洗与结构化原始节点树是很脏的直接喂给 Agent 效果很差。我一般会做几层清洗第一层是去重。同一个逻辑元素经常有父子节点都带相同文本的情况需要合并。我的规则是如果父节点和子节点文本相同且父节点只有一个子节点就保留子节点。第二层是过滤无效节点。宽高为 0 的、不可见isVisibleToUser为 false的、纯容器无信息的节点都去掉。第三层是语义补全。有些按钮没有 text 也没有 contentDescription但它的兄弟节点或父节点有描述可以往上找一层做推断。清洗完之后我会把节点整理成一个扁平列表每个元素带上坐标、文本、类型、可交互性这样 Agent 处理起来最方便。实测下来清洗后的节点列表比原始树的信息密度能提升好几倍。3.4 主通道的局限与失效场景无障碍树不是万能的我总结了几类它必然失效的场景这些就是像素通道要接管的边界自绘 UIFlutter、Unity、部分游戏引擎渲染的界面整棵树可能只有一个根节点。WebView 内容部分 WebView 没有正确暴露无障碍信息节点树是空的。图片型界面整个界面就是一张图没有任何文本节点。动态 Canvas图表、画板类应用内容全靠绘制。刻意隐藏有些应用出于安全考虑主动屏蔽无障碍信息。遇到这些场景主通道要么返回空要么返回的信息完全没法用这时候必须切像素通道。识别这些场景本身就是切换策略的一部分。4. 投屏像素通道的搭建与优化4.1 MediaProjection 抓帧的正确姿势像素通道的第一步是拿到屏幕画面用的是 MediaProjection。这套 API 的坑比无障碍多得多我一个个说。首先是权限申请。MediaProjection 需要用户手动授权而且每次授权只能用一个会话。授权流程必须走startActivityForResult拿到 resultData 之后才能创建MediaProjection对象。这里要注意Android 14 之后对 MediaProjection 的管控更严每次启动都要重新确认不能复用。其次是VirtualDisplay 的创建。抓帧的本质是把屏幕内容投射到一个虚拟显示上再通过 ImageReader 读取。VirtualDisplay virtualDisplay mediaProjection.createVirtualDisplay( ScreenCapture, width, height, densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, imageReader.getSurface(), null, null);width和height建议用真实屏幕分辨率不要缩放否则坐标映射会出问题。densityDpi用系统实际值。4.2 抓帧频率与性能平衡抓帧频率是个需要仔细权衡的参数。抓太快CPU 和内存扛不住抓太慢界面变化捕捉不到。我的经验值是按需抓帧而不是固定频率轮询。也就是 Agent 需要视觉信息的时候才抓一帧抓完立刻处理。这样平均开销最低。如果确实需要连续帧比如做动画分析我会控制在 5-10 FPS再高就没必要了手机界面变化没那么快。ImageReader 的maxImages设成 2 就够了设大了反而占内存。每次acquireLatestImage()拿到最新帧后处理完必须close()否则缓冲区会满后续抓帧直接失败。这个坑我踩过表现为抓几帧之后就卡住不动了。4.3 像素数据的预处理原始帧是 YUV 或者 RGBA 格式直接喂给视觉模型效果不好需要预处理格式转换YUV 转 RGB或者直接用 RGBA 输出。裁剪如果主通道已经给出了目标区域坐标就只裁剪那一块能大幅减少推理量。缩放视觉模型一般有固定输入尺寸按需缩放。压缩如果走网络推理需要压成 JPEG 减小传输量。裁剪这一步是双通道协同的关键。比如主通道告诉我屏幕下方有个按钮但没文本我就只裁剪下方那块区域去做视觉识别而不是整屏推理。实测这样能把单次推理时间从几百毫秒降到几十毫秒。4.4 视觉理解的能力边界像素通道能做什么、不能做什么心里要有数。它能做的是识别图片里的文字OCR、判断控件位置和类型、理解图标语义、分析界面布局。它做不好的是精确的文本内容OCR 有误差、复杂的层级关系、动态变化的实时追踪。所以像素通道的定位是补充信息不是替代主通道。它给出的结果要和主通道做交叉验证冲突的时候以主通道为准除非主通道明确失效。5. 双通道融合与协同策略5.1 坐标对齐与信息互补两个通道的坐标系必须统一。无障碍树的boundsInScreen是屏幕绝对坐标像素帧如果没缩放也是屏幕坐标两者可以直接对齐。如果做了缩放就要按比例换算回去。对齐之后可以做信息互补。典型场景是无障碍树给出了一个按钮的坐标框但按钮文本为空像素通道在这个坐标框内识别出提交两个字。两者一结合就得到了一个完整的元素描述坐标 语义 可点击性。这种互补比任何单通道都强。5.2 冲突消解规则两个通道偶尔会给出矛盾信息比如无障碍树说某处是按钮像素识别说是文本。我的消解规则优先级是结构化属性可点击、可编辑以无障碍树为准。文本内容以无障碍树为准除非主通道文本为空。视觉外观颜色、图标以像素通道为准。位置信息两者取交集不一致时以无障碍树为准。这套规则不是绝对的实际用的时候可以根据业务调整。核心原则是结构化信息可信度高于视觉推断因为前者是系统给的后者是模型猜的。5.3 缓存机制降低重复开销Agent 操作过程中很多界面元素是重复出现的。我会做一个短期缓存把最近识别过的元素坐标 语义 来源通道存起来下次遇到相同坐标或相同特征时直接复用不再重新走感知流程。缓存的有效期要短我一般设 2-3 秒因为界面随时可能变。缓存失效的触发条件包括界面发生明显变化节点树结构大变或像素差异超阈值、用户主动操作、超时。这个机制在连续操作场景下能省下大量重复计算。6. 实操中的常见问题与排查6.1 无障碍树读取为空怎么办这是最常见的问题。排查顺序我一般是这样的先确认服务是否真的连上了onServiceConnected有没有回调。然后检查getRootInActiveWindow()返回是不是 null如果是可能是当前窗口不支持无障碍或者权限没给全。再检查accessibilityFlags配置缺了FLAG_RETRIEVE_INTERACTIVE_WINDOWS会读不到多窗口内容。如果这些都正常但树还是空的那基本就是自绘 UI 了直接切像素通道别在无障碍上浪费时间。6.2 抓帧失败或黑屏MediaProjection 抓帧黑屏通常有几个原因VirtualDisplay 的 flag 设错了、ImageReader 的格式和 VirtualDisplay 不匹配、或者权限会话失效。我遇到过最隐蔽的一次是maxImages设成 1导致acquireLatestImage和acquireImage冲突表现为间歇性黑屏。改成 2 就好了。另外 Android 14 上如果应用切到后台MediaProjection 可能被系统回收需要重新申请。这个要在代码里做好监听和恢复。6.3 坐标偏移问题坐标偏移一般出现在做了缩放或者状态栏处理不当的时候。排查方法是打印无障碍树的坐标和像素帧的实际尺寸看两者比例是否一致。如果像素帧做了缩放所有坐标都要按原始尺寸/缩放尺寸的比例换算。状态栏和导航栏的高度也要考虑进去有些抓帧方式不包含系统栏坐标会整体偏移。6.4 性能瓶颈定位双通道方案性能问题一般出在像素通道。定位方法是分段计时抓帧耗时、预处理耗时、推理耗时分别打点。通常抓帧和推理是大头。抓帧慢就降频率或减分辨率推理慢就做裁剪或换更轻的模型。无障碍通道一般不会是瓶颈除非遍历逻辑写得太烂节点没回收导致 GC 频繁。问题现象可能原因排查方向无障碍树为空自绘UI/权限不足/flag缺失检查服务连接和flag配置抓帧黑屏flag错误/maxImages过小/会话失效检查VirtualDisplay配置坐标偏移缩放未换算/系统栏未计入对比两通道坐标比例响应慢抓帧频率高/推理未裁剪分段计时定位瓶颈内存增长节点未recycle/帧未close检查资源释放7. 我踩过的坑和几条实操心得先说一个最容易被忽视的点无障碍节点的 recycle 一定要做。我早期做高频遍历的时候没在意跑十几分钟内存就涨到几百兆最后直接崩。后来加了 recycle内存曲线立刻平稳。这个不是可选项是必须项。第二个心得是像素通道要懒加载。MediaProjection 的初始化和 VirtualDisplay 的创建都有开销不要一启动就常驻。我的做法是第一次需要视觉信息时才初始化用完一段时间没请求就释放。这样对不需要视觉的任务完全没有额外负担。第三个是别迷信视觉模型。我试过让视觉模型直接输出界面元素的完整描述结果发现它对文本的识别错误率比想象中高尤其是小字和特殊字体。后来改成视觉只做无障碍树做不了的事整体准确率反而上去了。视觉是补充不是主力这个定位一定要摆正。第四个是切换策略要留手动开关。自动判定再聪明也会有误判我在配置里留了强制走某个通道的开关调试和特殊场景下很有用。比如某个 App 无障碍树质量特别差就可以直接把它加进强制像素通道的名单。最后分享一个协同的小技巧用主通道的坐标框去约束像素通道的搜索范围。这个前面提过但值得再强调一次。整屏 OCR 和局部 OCR 的耗时能差一个数量级而主通道恰好能提供精确的局部坐标。两者配合既快又准。这个思路本质上就是结构化信息引导视觉计算是双通道方案里性价比最高的一招。后续如果要做扩展我会考虑加第三个通道——比如基于 UI 层级变化的差分感知专门捕捉动态界面的变化。不过那是另一个话题了双通道这套目前已经能覆盖我遇到的绝大多数场景。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑