ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AutoJS 手机自动化实战:控件定位、滑动翻页与稳定性排查

AutoJS 手机自动化实战:控件定位、滑动翻页与稳定性排查 手机上的重复操作做久了真的会让人怀疑人生——每天点开同一个 App、滑到同一个位置、点同一个按钮动作完全一样却必须手动完成。AutoJS 就是来解决这类问题的它是一套跑在 Android 设备上的 JavaScript 自动化框架靠系统的无障碍服务拿到界面控件信息再用脚本代替手指去点击、滑动、输入。这篇内容不讲空话我会把 AutoJS 从环境搭建、控件定位、滑动翻页一直讲到实际场景里的元素查找和稳定性排查把我自己踩过的坑一条条摊开。适合两类人看一类是写过一点 JS、想把手机操作自动化的开发者另一类是没写过代码但愿意照着例子改参数、能看懂逻辑的动手派。需要提前说清楚脚本能省下的是你自己的重复劳动不是拿来批量刷量、卡平台规则漏洞的这个边界后面我会展开讲。1. 先把问题想明白手机 App 自动化到底难在哪很多人对 AutoJS 的第一印象是能自动点屏幕于是上来就写click(500, 1000)跑两次能用第三次就点空了。这不是脚本写得差而是没搞清楚手机自动化的本质难点在哪。桌面端的自动化窗口位置固定、控件句柄稳定、屏幕分辨率不变手机端这三样全都在变——通知栏会弹出来遮住半屏、键盘会顶起布局、不同机型的分辨率差出好几倍、App 一次热更新就可能把按钮挪个位置。所以真正决定脚本能不能长期跑的不是点击那行代码而是你怎么找到要点的东西。1.1 三条技术路线的取舍坐标点击、控件驱动、图像识别手机自动化的定位手段基本就三条路各有各的适用面我在不同项目里三种都用过。坐标点击最简单click(x, y)一行搞定。它的前提是界面布局绝对固定。优点是不依赖任何权限、执行快缺点是脆得可怕换台手机、改个字体大小、弹个广告就废了。我一般只把它当兜底绝不作为主方案。控件驱动是 AutoJS 的核心能力。Android 的界面本质上是一棵控件树每个按钮、每个文本都有一个节点节点上带着id、text、desc、className、bounds这些属性。脚本可以通过选择器精确描述我要那个文字是立即购买的按钮然后拿到它的实时坐标再点击。界面挪位置没关系只要属性没变脚本就还能找到它。这条路最稳但要求你会看控件树。图像识别是第三条路靠截图做模板匹配或者找色。它的优势是能处理那些没有控件属性的东西——游戏画面、视频画面、Canvas 绘制的元素这些在控件树里往往只是一整块空白。代价是耗性能、对分辨率敏感、阈值调不好就误判。实际项目里我基本是控件优先、图像兜底、坐标最后的组合拳下面会具体讲怎么串起来。1.2 AutoJS 的能力边界以及它不适合干什么先说清楚它能干什么读取当前界面的控件信息、模拟点击和长按、模拟滑动和手势、输入文本、读取通知、截图、做图像处理、定时任务、读写本地文件、发 HTTP 请求。这些东西组合起来覆盖了绝大多数界面上的重复劳动。再说它干不了什么这点比上面更重要。它不能修改别人的 App、不能绕过登录验证、不能突破平台的风控策略它执行的是一个手脚很快的用户的操作而不是什么后台特权。所以凡是需要绕过破解批量的需求AutoJS 都不该被用在那里——一是平台规则不允许二是账号风险完全由使用者承担脚本本身没有任何保护能力。我个人给 AutoJS 划的适用范围只有一条处理自己账号下、平台允许手动完成的重复性操作。比如批量整理自己手机里的文件、把固定的信息录入到自己的记录表、给自己的设备做定时任务。越出这条线技术上的收益远小于风险不值得。2. 从零把环境跑通权限顺序错了会白折腾AutoJS 装完打不开脚本、开了无障碍还是提示未开启、运行两分钟就报服务断开——这些几乎全是权限顺序和系统省电策略的问题。我见过太多人卡在这一步就放弃了实际上只要按顺序把几项设置做对后面基本不会再管它。2.1 设备侧必须打开的几项设置关键顺序是这样的别跳步先允许安装来自未知来源的应用把 AutoJS 装上。打开无障碍服务。路径大致是设置 → 无障碍/辅助功能 → 已安装的服务 → 找到 AutoJS → 开启。第一次开启时系统会弹一个风险提示确认即可。这一步是核心控件读取和模拟点击全靠它。授予悬浮窗权限。调试时的悬浮控制台要靠它没有这个权限脚本跑起来你就只能干瞪眼。授予截图权限需要用到图像识别时才要。这个权限一般不能提前在设置里给得在脚本运行时通过requestScreenCapture()动态申请。把 App 加入电池优化白名单并在最近任务里锁定它。不同厂商叫法不同有的叫自启动管理有的叫后台运行权限。注意第 5 步是最容易被忽略、也最容易导致脚本跑着跑着就不动了的一步。系统在息屏或内存紧张时会回收后台进程无障碍服务一断所有控件操作全部失效脚本却不会报错只是安静地卡在那里。还有一个坑不要把 AutoJS 的省电策略设成智能限制或优化要设成无限制/不限制。这个设置在应用信息页的电池选项里改完之后建议重启一次 App 让服务重新绑定。2.2 编辑与调试方式的选择AutoJS 自带一个简易编辑器写小脚本够用但代码一多就很难受没有代码折叠、没有跳转定义、补全也弱。我的习惯是两种方式混用写复杂逻辑在电脑上用 VS Code 写好通过局域网文件同步或者直接复制粘贴到手机上的脚本文件里。调参数在手机自带编辑器里改因为要边跑边调来回同步太慢。调试输出我基本不用console.log弹窗而是用console.show()打开一个悬浮日志窗口然后用log()打时间戳和当前状态。悬浮窗的好处是脚本跑的时候你还能看到实时输出不用退出 App。// 打开悬浮日志窗口方便观察运行状态 console.show(); console.setSize(device.width * 0.9, device.height * 0.3); function logStep(msg) { log([ new Date().toTimeString().slice(0, 8) ] msg); }时间戳这一步看着不起眼但排查卡在哪一步的时候有没有时间戳完全是两种效率。我踩过一次坑脚本在某个循环里静默卡了四十分钟因为没有日志只能从头重跑复现。2.3 第一个能跑起来的脚本别一上来就写复杂逻辑先用五行代码确认环境是通的// 等待无障碍服务就绪最多等 10 秒 auto.waitFor(); toast(服务已就绪); logStep(屏幕尺寸: device.width x device.height); // 打开设置页验证控件读取和启动能力 app.launchPackage(com.android.settings); sleep(2000); // 试着读取当前界面上所有可见文本 var texts textMatches(/./).find(); logStep(当前界面文本节点数量: texts.length);这段跑通说明无障碍、启动应用、控件读取三条链路都正常。texts.length如果是 0别怀疑代码回去检查无障碍服务是不是真的开了——有时候系统显示已开启实际进程已经被回收需要关掉再开一次。3. 控件定位整个自动化里最吃功夫的一环脚本写得漂不漂亮无所谓定位准不准才是生死线。我做过一个统计一个稳定的自动化脚本调试时间大概有七成花在找元素上。这一节把控件定位的方法论拆开讲。3.1 用布局分析工具读懂控件树AutoJS 自带布局分析功能开启后会在屏幕上显示一个悬浮球点开能看到当前界面的层级结构点任意一个控件就能看到它的全部属性。这个工具用熟之后定位效率能提升好几倍。看控件树的时候我会重点关注四个属性属性含义稳定性id资源 ID如com.xxx:id/btn_ok最稳定但很多 App 混淆后不可读text显示的文字内容较稳定但多语言版本会变desc内容描述图片按钮常靠它标注意图较稳定适合无文字图标按钮bounds控件在屏幕上的矩形范围每次布局都可能变只用于取坐标id是首选因为它跟显示文字无关切语言也不受影响。但国内的 App 很多做了资源混淆id变成a1b、c2d这种无意义字符串而且每次发版还会变这种情况就只能退到text或desc。还有一个技巧如果要点的是一个图标按钮它本身没有text但通常它的父节点或者旁边的兄弟节点有文字标签。这时候可以用className加上boundsInside限定区域来筛比盲目全屏找要快得多。3.2 选择器语法与几种常用匹配策略AutoJS 的选择器写法很直观字符串参数匹配text带Contains的是包含匹配// 精确匹配 var btn text(立即领取).findOne(3000); // 包含匹配适合文字前后带数字或空格的情况 var btn2 textContains(领取).findOne(3000); // 正则匹配适合文字格式固定但内容变化的情况 var btn3 textMatches(/剩余\d次/).findOne(3000); // 多条件组合类名 可点击 var btn4 className(android.widget.Button).clickable(true).findOne(3000);几个我反复验证过的经验findOne()一定要给超时参数。不给的话默认是无限等待一旦元素不存在脚本就永久挂死在那里连日志都不打。优先用textContains而不是text。界面上经常出现领取后面跟个角标数字或者前后带空格精确匹配反而找不到。匹配结果可能有多个时用find()拿到数组再按bounds()的位置排序取最靠上或最靠左的那个避免点到列表里重复出现的同名按钮。需要操作一个不可点击的节点时可以往上找它的父节点node.parent()或者用clickable(true)反查最近的祖先。我遇到过很多次点击无效最后发现点的是TextView真正能响应的是它外面包的那层LinearLayout。3.3 控件找不到时的兜底图像匹配与坐标回退再怎么优化选择器也总有一些元素在控件树里查不到——视频画面上的浮层、游戏渲染的元素、被 WebView 绘制出来的东西。这时候就得上图像识别。图像匹配的流程是先截取一张目标元素的小图作为模板存到本地运行时截图整屏用模板去匹配拿到匹配区域的中心坐标。// 申请截图权限只需一次 if (!requestScreenCapture()) { toast(截图权限被拒绝); exit(); } // 读取模板图 var template images.read(/sdcard/tpl_btn.png); // 截取当前屏幕 var screen captureScreen(); // 模板匹配similarity 是相似度阈值 var point findImage(screen, template, { threshold: 0.85 }); if (point) { click(point.x template.width / 2, point.y template.height / 2); } else { logStep(模板未匹配到目标); } // 及时回收否则内存会涨得很快 screen.recycle(); template.recycle();这里有几个必须注意的点注意模板图必须从目标设备本身截取不要用另一台手机截的图。不同分辨率、不同屏幕密度下同一个按钮的像素尺寸可能差一倍模板匹配直接失败。注意threshold不要一上来就设 0.95。界面有渐变、有半透明遮罩、有轻微压缩噪声阈值过高会频繁匹配失败。我的经验起点是 0.8稳定后再往上加。坐标回退是最后一招只在界面完全固定且无法用其他方式定位时用而且必须写成相对坐标// 相对坐标避免不同分辨率下失效 var x device.width * 0.5; var y device.height * 0.85; click(x, y);图像识别的性能开销不小一帧匹配在中端机上可能要几百毫秒。所以不要在死循环里高频调用通常是先跑控件查找找不到再走图像两三次失败就退出别硬耗。4. 滑动翻页为什么你的脚本总是滑不动滑动看着最简单实际上是我被问得最多的一个问题。我写了 swipe脚本也执行了但页面纹丝不动——这类问题九成出在参数上而不是代码本身。4.1 swipe 和 gesture 到底差在哪这两个 API 表面上都是滑动底层机制差别很大。swipe(x1, y1, x2, y2, duration)是模拟一次带速度的手势。关键在于duration这个参数duration很短比如 100 到 200 毫秒时系统会把它识别为一次快速滑动带惯性松手后页面会继续滑行一段距离再停下。duration中等500 毫秒左右时接近手指匀速拖动的效果惯性较小。duration很长超过 1000 毫秒时系统可能把它识别成长按拖动某些列表会触发选中或者拖拽排序完全不是你想要的效果。gesture(duration, [x1, y1], [x2, y2], ...)则是按你给定的路径点均匀分配时间几乎不产生惯性滑到哪就停在哪。需要精确控制滑动距离的时候我基本都用它。// 带惯性的快速滑动适合刷信息流这种需要连续翻页的场景 swipe(device.width / 2, device.height * 0.75, device.width / 2, device.height * 0.25, 300); sleep(500); // 精确滑动滑多少就走多少适合必须停在第 N 屏的场景 gesture(600, [device.width / 2, device.height * 0.75], [device.width / 2, device.height * 0.25]);选择逻辑很简单要翻页快、不用管停在哪用swipe短时长要每次滑动的距离一致、可预测用gesture。4.2 滑动起止点与距离的计算滑动起止点不能随便写踩过的坑都在这几个地方起点不要贴边。很多手机在屏幕底部三分之一区域有系统返回手势顶部有下拉通知栏的手势。如果你的起点落在这些区域滑动会被系统截走App 根本收不到事件。我的经验值是纵向留出 15% 的安全边距。滑动距离不要太大。一次滑过屏幕高度的 60% 以上页面可能直接跳过你想要的区域甚至触发回到顶部的快捷操作。半屏左右是比较稳的距离。横向滑动的边界同理。左右滑动通常要避开屏幕左右各 10% 的区域那里是系统返回手势的触发带。综合下来我常用的翻页参数是这样的var startY device.height * 0.72; // 起点偏下避开中间内容区的按钮 var endY device.height * 0.28; // 终点偏上避免触碰顶部标题栏 var midX device.width * 0.5; // 横向居中避开左右边缘手势 // 向上翻页看后面的内容 gesture(500, [midX, startY], [midX, endY]); // 向下翻页往回看把起止点反过来 gesture(500, [midX, endY], [midX, startY]);autojs往上翻页这个需求对应的其实就是第二种——起止点反过来写。方向搞错是最低级的错误但真有人调了半天参数才发现是方向反了。4.3 循环翻页的终止条件怎么写翻页本身不难难的是什么时候该停。写个while(true)死循环翻下去轻则耗电耗流量重则触发 App 的异常检测。终止条件我一般用三种组合第一种目标出现即停。每翻一页检查一次目标元素是否存在var found false; for (var i 0; i 30; i) { if (textContains(目标关键词).exists()) { found true; logStep(第 (i 1) 页找到目标); break; } gesture(500, [midX, startY], [midX, endY]); sleep(800); }第二种页面不再变化即停。到底了还继续滑页面内容不变这就是终止信号。判断方式是比较滑动前后某个标志性元素的坐标或文本var lastAnchor ; var sameCount 0; while (sameCount 3) { var anchor ; var node className(android.widget.TextView).findOnce(); if (node) anchor node.text(); if (anchor lastAnchor anchor ! ) { sameCount; } else { sameCount 0; lastAnchor anchor; } gesture(500, [midX, startY], [midX, endY]); sleep(800); } logStep(连续 3 次无新内容判定已到底);第三种次数上限硬约束。不管有没有满足前两个条件最多翻 N 页就退出。这是保命机制防止逻辑判断出错时无限跑下去。提示三种条件建议同时用。次数上限是兜底页面无变化是正常终止目标出现是提前结束。只写其中任何一种都会在某些分支场景下出问题。另外一个细节是sleep的时长。滑动之后要给页面留出渲染时间800 毫秒是个比较保守的值。设太短的话你读到的是上一页的控件树判断自然全错。这个坑我在做信息流翻页时踩过好几次日志里显示检测到目标截图一看还在上一屏。5. 实战拆解定位直播间里的浮动元素autojs 找抖音福袋的位置这个搜索词背后其实是一个很典型的 UI 定位难题目标元素是浮动的、位置随机、出现时间有限、还会被其他元素遮挡。这类问题的解法思路可以推广到所有动态浮层定位的场景。需要提前说明下面讲的纯属 UI 定位的技术思路实际使用请遵守对应平台的使用规则不要做批量或多账号操作。5.1 为什么固定坐标在这类场景一定翻车直播间的浮动元素有几个特性决定了坐标方案必然失效位置随机。浮层可能出现在屏幕右下、左下、或者中部悬浮不同直播间、不同时间的位置都不一样。写死一个click(800, 1200)可能十次里对三次。尺寸随内容变化。浮层上带着倒计时数字或者状态文字数字位数变化时宽度会变控件本身的bounds也跟着变。会被遮挡。弹幕、礼物特效、连麦窗口都可能盖在它上面导致你点下去点到的是别的东西。有时效性。它可能只在特定时间窗口内存在脚本慢了就消失了这时候再去点操作落到的是底下的内容区可能触发完全无关的动作。所以正确的思路是实时定位每次操作前都重新查一次它的当前坐标而不是缓存第一次找到的位置。5.2 多策略组合定位与超时重试我处理这类元素的套路是三层查找逐层降级function locateFloatingElement(maxWaitMs) { var deadline new Date().getTime() maxWaitMs; while (new Date().getTime() deadline) { // 策略一控件特征匹配最稳 var node descContains(福袋).findOnce() || textContains(福袋).findOnce(); if (node) { var b node.bounds(); if (b.width() 0 b.height() 0) { logStep(控件定位成功: b.centerX() , b.centerY()); return { x: b.centerX(), y: b.centerY(), by: control }; } } // 策略二图像模板匹配兜底 var tpl images.read(/sdcard/tpl_fudai.png); if (tpl) { var screen captureScreen(); var p findImage(screen, tpl, { threshold: 0.82 }); screen.recycle(); tpl.recycle(); if (p) { logStep(图像定位成功: p.x , p.y); return { x: p.x tpl.width / 2, y: p.y tpl.height / 2, by: image }; } } sleep(300); } logStep(超时未找到目标); return null; }这段代码里有几个设计意图值得单独说为什么用findOnce()而不是findOne()findOne自带等待在里面套一层 while 循环会导致等待时间层层叠加实际响应反而变慢。用findOnce()立即返回由外层循环控制节奏超时时间是可控的。为什么每次循环都重新读模板图理论上可以提到循环外读一次但实际调试时我保留了在内部读取的写法方便替换模板。正式跑的时候应该提到外面避免重复 IO。为什么循环间隔是 300 毫秒太密会占满 CPU导致截图变慢、反而错过时机太疏可能元素出现后 1 秒才被检测到窗口期就过了。300 毫秒是我实测比较平衡的值。为什么判断bounds的宽高有些节点虽然存在但处于不可见或者零尺寸状态bounds()返回的是空矩形直接取中心点会点到(0,0)。这个判断帮我避开过好几次莫名其妙的点击。5.3 点击之后的确认动作点完就完事是脚本出 bug 的主要来源之一。点击这个动作本身只表示事件发出去了不代表操作成功了。所以每次点击之后都要有一个确认动作var target locateFloatingElement(8000); if (target) { click(target.x, target.y); sleep(600); // 确认动作看目标元素是否消失或者出现了预期的结果提示 var stillThere descContains(福袋).findOnce(); if (!stillThere) { logStep(点击生效目标已消失定位方式: target.by); } else { logStep(点击可能未生效准备重试); // 这里可以加一次重试但最多一次避免陷入循环 } }确认逻辑要看场景而定。有些场景是目标消失代表成功有些是新页面出现代表成功还有的是出现了一个 toast 提示。核心原则是任何一个有副作用的操作都必须有一个可观测的确认信号。没有确认信号的自动化脚本出问题时你连是哪一步错了都不知道。另外补一个细节click()有返回值返回true表示事件成功注入。但这个返回值只能说明事件发出成功不能说明业务处理成功。真正可靠的是上层的界面状态判断别把click的返回值当成成功标志。6. 稳定性与问题排查实录脚本能跑通一次和能稳定跑一个月中间隔着一整个工程实践的距离。这一节是我整理的问题清单和几条长期跑下来的习惯。6.1 常见故障速查表现象可能原因排查方向脚本完全不执行无障碍服务未开启或已断开检查服务状态重开一次确认已加入电池白名单findOne一直返回 null元素还没渲染出来选择器写错加sleep等待用布局分析工具核对属性点击无反应点到的是不可点击的子节点坐标被遮挡改用clickable(true)找父节点检查是否有弹窗遮挡滑动无效果起止点落在系统手势区duration 参数不当起止点内缩 15%改用gesture精确滑动图像匹配一直失败模板图来自其他设备阈值过高用本机重截模板阈值降到 0.8 试跑一段时间后卡死内存泄漏截图对象未回收及时recycle()定期调用images.releaseScreenCapture()息屏后脚本停止系统省电策略回收进程关闭电池优化锁定后台必要时保持屏幕常亮滑动后判断出错渲染延迟读到的是上一页数据把sleep从 800 毫秒提到 1200 毫秒试这张表里的每一条都是我自己撞过的。其中点击无反应是最耗时间的因为现象是沉默的——脚本不报错只是什么都没发生。我的排查顺序是先用日志打出节点的bounds确认矩形位置和大小正常再打出clickable属性最后用布局分析工具在手点的情况下看看到底激活的是哪个节点。6.2 让脚本跑得久的几个工程习惯第一所有等待都要有上限。findOne()给超时、while循环给最大次数、waitFor()给超时时间。任何没有上限的等待都是定时炸弹。第二关键节点打日志但要节制。我习惯在定位成功/失败点击前后每轮循环开始这三个位置打日志。频率太高的日志比如每次截图都打会拖慢执行也会把有用信息淹掉。第三资源一定要回收。图像处理是内存消耗大户captureScreen()拿到的对象和images.read()读到的模板都占内存用完必须recycle()。我做过对比同一段跑两小时的脚本加回收和没加回收内存占用能差好几倍没回收的那个基本撑不过一小时。第四把易变的参数抽出来放开头。屏幕比例、等待时长、重试次数、相似度阈值这些全都提到脚本顶部集中定义。App 更新后要调整改一行就够了不用在几百行代码里翻。// 可调参数集中区 var CFG { swipeStartRatio: 0.72, // 滑动起点纵向比例 swipeEndRatio: 0.28, // 滑动终点纵向比例 swipeDuration: 500, // 滑动时长毫秒 renderWait: 800, // 翻页后等待渲染的时间 maxPages: 30, // 最大翻页次数 matchThreshold: 0.82, // 图像匹配相似度阈值 retryLimit: 3 // 单步最大重试次数 };第五异常要捕获不能让整个脚本崩掉。用try...catch把主循环包起来出错时记录日志、释放资源、决定是继续还是退出。自动化脚本运行时间长环境变化多没有任何异常处理的脚本基本上活不过一天。try { mainLoop(); } catch (e) { logStep(运行异常: e); // 释放图像资源 images.releaseScreenCapture(); } finally { logStep(脚本结束); }第六别把频率拉满。不管什么场景操作间隔都留出自然的停顿。一方面是为了等界面渲染另一方面也是让脚本的行为更接近正常使用节奏。把点击间隔压到几十毫秒除了让设备发烫、更快触发异常没有任何好处。我个人在长时间跑的脚本里还有个小习惯加一个健康检查计步器每跑满 100 轮就在日志里打一次运行时长和当前内存占用。这个习惯帮我抓到过一次内存缓慢增长的问题——脚本跑三小时后开始变卡日志显示内存曲线一直在涨最后定位到是某处截图忘了解析释放补上之后就稳了。自动化脚本的稳定性问题十有八九都藏在这种细节里而日志是唯一能把它们挖出来的工具。
返回列表