自动化脚本的稳定性,很大程度上取决于“找到目标元素”这件事是否可靠。无障碍控件查找在标准 Android 应用上工作得很好,但一旦进入游戏、自绘渲染界面、Flutter 或某些加固过的应用,getUiTree 返回空树或残缺节点是常有的事。控件的 text 和 id 属性不存在,findView 就无从谈起。这时继续在控件查找的框架里打补丁,收益很低;更务实的做法是把定位层整体切换到 OCR,用屏幕上的文字本身作为锚点。
冰狐的 OCR 接口不是单一函数,而是一组按用途分层的工具。底层是 ocr() 和 tessOcr(),分别面向中文优化和轻量数字/英文场景;之上封装了 ocrFindView()、ocrClick()、ocrSwitchPage()、ocrBack2Page(),把“识别—定位—操作”这条链路压缩成一两行调用。本文不重复官方文档的参数表,而是从一个脚本编写者的角度,讨论选型依据、实际代码写法,以及那些在官方示例之外容易踩到的工程细节。
先选引擎,再谈写法
冰狐提供三种识别路径,选错引擎会让后续所有优化事倍功半。
原生离线 ocr() 是中文自动化场景的默认选项。 它在手机本地运行,针对汉字做了专项优化,开启 awesome 增强后汉字识别率可以推到接近 100%,代价是每次识别多出半秒左右的耗时。对于大多数 App 自动化——登录、表单填写、列表点击——这个延迟完全可以接受。数据全程留在设备本地,不需要网络,批量跑脚本时也不会因为服务器抖动而中断。
tessOcr() 的适用范围比想象中窄。 它基于 Tesseract,对纯数字、英文短文本(验证码、价格、倒计时)够用,但中文界面下错误率明显高于 ocr(),而且不支持 awesome 增强。如果你只是识别一个六位数字验证码,用它比 ocr() 更轻;如果是中文按钮,不要在这一层做妥协。
服务端 OCR 解决的是精度上限问题。 当界面文字极小、对比度低、或者字体经过艺术化处理,手机端引擎反复出错时,ocr({ online: { url: '...' } }) 可以把截图发到自建服务器识别,返回结果的质量通常会有可见提升。它的代价是网络依赖和延迟,适合关键步骤(如金额确认、验证码)而非高频循环。如果整个脚本都切到服务端,吞吐量会受网络往返限制。
环境准备:两行代码,一行权限,一行线程
所有 OCR 调用的前置条件只有一个:requestScreenShot()。不调用它,第一次 ocr() 就会因缺少截屏权限而失败。这个调用放在 main() 的第一行即可。
真正容易被忽略的是线程栈。冰狐的脚本默认运行在主线程上,OCR 不会有什么问题。但如果你把识别逻辑放进 Thread——比如一边识别一边维持 UI 交互——线程的默认栈大小不足以支撑 OCR 的内存开销,结果是脚本闪退或识别进程被静默终止。官方给出的安全值是 12388608,在线程创建时显式传入 stackSize 参数:
function main() {
requestScreenShot();
var worker = startThread(function () {
var ret = ocr({ region: [0, 0.6, 1, 0.4] });
for (var i = 0; i < ret.length; i++) {
console.log(ret[i].text, ret[i].region);
}
}, [], 12388608);
}
如果你不打算用多线程,这一行可以跳过;但一旦决定用,就不能省。
返回值是数组,坐标是全局的
ocr() 返回一个数组,每个元素形如 { text: '数据分析', region: [100, 200, 180, 60] }。region 的格式是 [left, top, width, height],而且始终相对于整个屏幕,而不是相对于你在 region 参数里划定的识别区域。这意味着你可以直接用返回的坐标去 click(),不需要做偏移换算。很多人在第一次使用时会把识别区域的左上角坐标加到返回坐标上,结果点击位置整体偏移,这是对坐标语义的误解。
region 参数本身支持两种坐标制:数值在 0 到 1 之间时按屏幕比例解释,大于 1 时按像素解释。比例坐标更适合需要适配不同分辨率设备的脚本,也是官方在实践中更推荐的做法。比如只识别屏幕下半部分:
var ret = ocr({ region: [0, 0.5, 1, 0.5] });
缩小识别区域不只是“减少干扰”,它直接缩短了 OCR 引擎需要处理的像素量,在高频循环中能带来可感知的速度差异。
从“识别文字”到“点击文字”:优先用封装函数
拿到 ocr() 的数组之后手动遍历匹配文字再计算坐标点击,不是不行,但在“找文字并点击”这个最高频的操作上,冰狐已经封装了 ocrClick()。它的参数接受 tag 表达式,支持 txt: 精确匹配、txt^: 前缀匹配、txt*: 包含匹配,多个文本用 | 连接,找到任意一个即触发点击。
一个自动登录场景的典型写法:
function loginIfNeeded() {
requestScreenShot();
var clicked = ocrClick('立即登录|登录', {
region: [0, 0.4, 1, 0.6],
maxStep: 3,
afterWait: 800
});
if (!clicked) {
console.log('未找到登录入口');
return false;
}
sleep(1500);
ocrClick('手机号', { afterWait: 300 });
inputText('13800138000');
ocrClick('密码', { afterWait: 300 });
inputText('placeholder');
ocrClick('登录', { maxStep: 2, afterWait: 1500 });
return true;
}
maxStep 和 afterWait 这两个参数值得单独说。maxStep 控制失败后的重搜次数,默认值偏保守;页面加载有延迟时,把 maxStep 调到 3 到 5 比在外层手写 while 循环更简洁。afterWait 则是在点击成功后等待的毫秒数,给界面切换留出时间,避免下一次识别截到过渡动画中的模糊帧。
当你需要比“点击第一个匹配项”更精细的控制时,ocrFindView() 是更底层的选择。它返回匹配到的对象(包含 tag、text、region),不自动执行点击,把操作权交回脚本。比如只点击第二个匹配项:
function clickSecond(keyword) {
var found = ocrFindView('txt*:' + keyword, {
region: [0, 0.3, 1, 0.7],
flag: 'find_all'
});
if (!found || !found.views || found.views.length < 2) {
return false;
}
var target = found.views[1];
var r = target.region;
click(r[0] + r[2] / 2, r[1] + r[3] / 2);
return true;
}
这里 flag: 'find_all' 让 ocrFindView 返回所有匹配项而不是第一个,返回结构从单个对象变为 { tag: '', views: [...] }。拿到 region 后手动取中心点点击,比 ocrClick 多了一步,但换来了“点击哪一个”的完全控制权。
准确率不是“开不开 awesome”的问题
awesome 参数确实有效:{ threshold: 0.9, count: 3 } 是常见的起点,threshold 取 0.5 到 0.9 之间,count 取 2 到 6,数值越高识别越保守、耗时也越长。但把所有场景都无脑开满并不是最优策略。对于字体清晰、背景干净的界面文字(比如大多数 App 的设置页按钮),默认的 ocr() 已经足够,开 awesome 只是白白增加延迟。
真正需要 awesome 的是那些“差一个字就点错”的场景:金额确认、订单状态判断、相似度极高的按钮文本(“确认”和“确认收货”)。在这些地方,半秒的代价换来的是避免一次不可逆的误操作。
如果 awesome 开启后仍然有个别汉字反复出错,冰狐提供了增量学习通道:把正确文本加入 ocr 文本库,在线训练后下载 model.bin 打包进 APK,新模型就会在设备本地生效。这个机制适合固定界面上的高频错字,不适合频繁变化的动态内容。工程上更稳妥的做法仍然是限定 region:把识别范围压到目标文字附近的最小矩形,减少无关字符对识别引擎的干扰,这比任何后处理纠错都更直接。
工程上需要保持清醒的几点
OCR 的耗时显著高于控件查找。一个全屏 ocr() 在中等性能设备上通常在数百毫秒到一秒之间,而 findView 往往在几十毫秒内返回。因此不要把 OCR 放进无条件的高频 while 循环里。合理的节奏是:先尝试控件查找,失败后再落到 OCR;OCR 步骤之间保留 sleep 或利用 ocrClick 自带的 timeout 和 maxStep 做有限重试,而不是自己写密集轮询。
倾斜文字是另一个边界。冰狐的离线 OCR 默认识别水平文本,明显倾斜或艺术化排列的文字识别率会急剧下降,这不是参数能完全解决的问题。遇到这类界面,回到截图预处理或服务端识别才是现实路径。
最后,OCR 返回的 text 是识别结果,不是界面原文。模糊字体、背景干扰、相似字形都可能让 "确认" 变成 "确 认" 或 "确以"。在判断逻辑里用 txt*:(包含匹配)而不是 txt:(精确匹配),能吸收一部分这类噪声;但如果你需要的是“这个按钮是否存在”的确定性判断,ocrFindView 的 failed 回调或 ocrClick的布尔返回值,比自己去解析 ocr() 数组更直接。
把 OCR 看作定位层的一个可选后端,而不是一个“识别文字的工具”,脚本的结构会清晰很多:先判断当前界面是否有可用的控件树,有则走轻量的原生查找;没有则切换到 OCR,用 region 限定成本、用封装函数完成操作、用 awesome 和增量学习兜住精度。控件树失效不意味着自动化脚本失效,它只是意味着你需要换一套坐标体系来“看见”界面。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/ai_coder_ai/article/details/170328665



