
1. 先想清楚手机app自动化到底在自动化什么1.1 Autojs入门它不是一个点击器而是一套能看懂界面的脚本引擎很多人第一次听到 Autojs脑子里浮现的画面是那种固定在屏幕某个坐标、机械重复点一下的连点器。这两者差别其实挺大。连点器只知道屏幕第 800 行第 400 列有个东西界面一变、分辨率一换它就直接点空而 Autojs 是跑在 Android 无障碍服务之上的一层脚本运行时它能读取当前界面的控件树知道屏幕上现在有哪些节点、每个节点的文字是什么、能不能点击、显示区域在哪也能反过来模拟点击、滑动、返回、输入这些动作。脚本语言用的是 JavaScript也就是说你写的不是配置项而是带判断、循环、函数、异常处理的程序。这件事的意义在于手机app自动化从盲操作变成了带眼睛的操作。你可以写找到写着我的的那个节点点它而不是点屏幕右下角。前者换个手机、换个字号、换个版本大概率还能跑后者只要界面动一下就得重新录一遍。这也是我后来很少再用坐标型工具的原因维护成本差太多。Autojs 这类工具在圈子里主要有几个用途方向一是把日常重复性的手机操作打包成脚本比如每天定时打开某个应用做签到这类机械动作二是做界面流程的功能验证产品改版后跑一遍脚本看看关键路径有没有断三是辅助操作把一些层级很深、对不熟悉智能设备的人特别不友好的路径做成一键执行四是纯学习用它来理解 Android 的控件树结构和无障碍服务机制这个收获其实比脚本本身还大。需要提前说清楚的一点是这类工具的能力再强也只是替你在你自己设备上操作。它不能绕过任何应用自身的规则限制也不该被用来做批量刷取、虚假交易、抢购囤货这类明显违背平台约定的行为。我后面讲的每一个技巧都建立在这个边界之内。1.2 谁适合看这篇谁看了会失望如果你的需求是我每天要在手机上来回点十几下完成一件固定的事我想把它变成一下那 Autojs 非常适合你而且学习曲线没有想象中陡。你不需要会 Android 开发只要会基本的 if、for、函数能看懂 JSON 那种结构就够了。我见过不少做运营、做测试、做数据处理的朋友本身不是程序员用了一两周也能写出稳定可用的小脚本。但如果你期待的是写个脚本躺着产生收益那大概率会失望。自动化脚本解决的是重复劳动的效率问题它不解决商业模式的合法性问题。凡是涉及批量注册、批量互动、虚假流量的场景脚本写得再花哨本质上也是在跟规则对抗收益和风险完全不成比例我不建议任何人往这个方向投入时间。2. 动手之前的准备工作与整体设计思路2.1 运行环境怎么搭权限比代码更容易卡住人先把环境说透因为新手 80% 的挫败感来自权限没配好而不是代码写错。你需要准备的东西不多一台 Android 手机真机优先模拟器在部分应用上会有环境检测差异、一个 Autojs 类的运行环境目前社区里有多个分支比如 AutoX.js 这类开源版本功能大同小异选一个仍在维护的即可、以及一台电脑用来写代码和传文件当然直接在手机上写也不是不行但屏幕小、调试累。安装完之后必须逐项确认下面这些权限缺一个脚本就可能半夜自己断掉权限项作用没开会出现什么无障碍服务读取控件树、模拟点击滑动脚本完全跑不起来findOne 永远返回 null悬浮窗显示控制面板、停止按钮脚本失控时你没法快速停掉它后台运行 / 自启动防止系统清理进程切到别的应用后脚本被冻结忽略电池优化防止系统休眠省电杀进程跑十几分钟后无声无息地停住存储读写写日志、存截图、读配置日志为空出问题无从排查这里有个实测经验不同厂商的系统在后台管理上差异极大有的系统即使开了忽略电池优化只要你锁屏几分钟进程还是会被压掉。稳妥的做法是调试阶段把屏幕常亮打开开发者选项里就有等脚本稳定了再去研究保活问题。还有一个细节很多手机的无障碍服务在系统更新或强制停止应用后会被自动关闭所以脚本启动时最好先做一次自检判断无障碍是否可用不可用就直接弹出提示别让它悄无声息地失败。2.2 脚本整体设计思路别从怎么写开始从画流程开始我踩过最大的一个坑是一上手就写代码。结果写着写着发现流程里有七八个分支情况没考虑到代码变成一团乱麻改一处崩三处。后来我的习惯改成先在纸上把流程画成状态节点。举个最常见的例子一个打开应用做几件固定事情的脚本我通常拆成这几个状态启动应用、等待首屏加载完成、判断是否登录、执行主任务、处理意外弹窗、收尾退出。注意处理意外弹窗是被单独拎出来的因为它是自动化脚本里最容易翻车的地方——广告弹窗、版本更新提示、限时活动浮层任何一个弹出来都会把你的后续步骤全部打偏。状态划分完之后每一步再问自己三个问题这一步的完成标志是什么超时多久算失败失败了是重试还是跳过把这几个问题填上答案代码结构基本就出来了。这个思路听起来有点啰嗦但它能省掉大量后期返工。说白了自动化脚本的难点从来不是点击这个动作而是我怎么知道现在该做哪一步。2.3 三种定位方式的选择逻辑控件、坐标、图像各管一段定位是 Autojs 这类工具的核心能力也是决定脚本寿命的关键。我一般按优先级排三档第一档是控件定位也就是通过文字、id、class、描述等属性找到节点。这是最稳的方式。写法通常长这样// 按文字精确匹配等待最多 3 秒 var btn text(签到).findOne(3000); if (btn) { btn.click(); }或者更宽松一点// 文字包含关键词适合文案会微调的场景 var item textContains(立即).findOne(3000);第二档是坐标定位也就是拿到控件的显示区域后取中心点点击var node text(确定).findOne(2000); if (node) { var b node.bounds(); click(b.centerX(), b.centerY()); }注意即便是坐标点击我也强烈建议坐标是从控件上推导出来的而不是硬编码的固定数字。硬编码坐标的脚本换台设备就废。第三档是图像匹配用截图找图的方式定位。它适合两类情况一是控件树里压根读不到的元素比如某些游戏画面、某些用自绘引擎渲染的界面二是需要判断视觉状态而不是结构状态的场景。图像匹配的代价是慢、吃性能、对分辨率缩放敏感所以能不用就不用。选择逻辑就这么简单能读控件就不点坐标能点坐标就不找图。这句话我建议刚入门的人直接贴在屏幕上。3. 核心能力实操滑动翻页、动态控件定位与稳定性设计3.1 往上翻页与滑动翻页三种写法对应三种场景滑动翻页是手机app自动化里出现频率最高的动作几乎每个脚本都会用到。但同样是往上翻不同场景的写法完全不同用错了就会出现滑过头、滑不动、重复滑同一屏这些问题。最常见的写法是直接调用滑动指令给定起点和终点// 从屏幕下方往上滑实现向上翻页 swipe(device.width / 2, device.height * 0.75, device.width / 2, device.height * 0.25, 600);这里用比例而不是固定像素是为了适配不同分辨率的设备。600 是滑动时长单位毫秒。这个参数很关键时间太短系统会识别成快速甩动页面可能带着惯性滚出去好几屏时间太长会被识别成缓慢拖拽有时候反而滑不动。我的实测经验是普通列表翻页用 400 到 700 毫秒比较稳需要精确控制滚动距离时用 800 到 1200 毫秒。第二种是基于可滚动控件的方法。如果当前界面上有一个可滚动的容器节点可以直接让它执行翻页动作var list className(android.widget.ListView).findOne(3000) || scrollable(true).findOne(3000); if (list) { list.scrollForward(); sleep(800); }这种写法比手写 swipe 更语义化因为它不关心滑动距离只关心往下滚一屏。缺点是部分应用自定义了滚动容器识别不到这时候就只能退回手写滑动。第三种是惯性甩动适合那种需要快速浏览大量内容的场景// 短距离快速滑动利用惯性连续滚动 swipe(device.width / 2, device.height * 0.8, device.width / 2, device.height * 0.2, 100); sleep(1000);不管用哪一种翻页循环里必须解决一个问题怎么判断已经到底了。我的做法是记录每一屏的内容特征通常是页面上所有可见文字的拼接值或者某个关键节点的数量如果连续两三次滑动后特征没变化就认为到底了跳出循环。比死循环指定次数要可靠得多。var lastMark ; var sameCount 0; while (sameCount 2) { swipe(device.width / 2, device.height * 0.75, device.width / 2, device.height * 0.3, 600); sleep(1200); var mark collectVisibleText(); // 自行实现抓取当前屏可见文字 if (mark lastMark) { sameCount; } else { sameCount 0; lastMark mark; } }注意翻页循环里千万别省略 sleep。滑动之后界面有动画和网络加载你立刻去抓内容拿到的往往是上一屏的残留状态会导致重复处理和漏处理。这个坑我刚入门时踩了整整两天。3.2 找动态浮层控件的位置以直播间福袋这类元素为例像直播间里的互动道具这种元素是定位练习里非常典型的案例因为它同时具备三个难点出现时机随机、位置会漂移、文案可能变化。先说出现时机。这类元素不是一进页面就有的可能要等几秒甚至十几秒。所以定位时不能只用 findOne 一次得用轮询。我一般写成这样function waitForAny(selectors, timeout) { var start Date.now(); while (Date.now() - start timeout) { for (var i 0; i selectors.length; i) { var node selectors[i].findOnce(); if (node) return node; } sleep(500); } return null; }传进去的 selectors 是一个候选数组比如[text(福袋), textContains(福袋), descContains(福袋)]。为什么要给多个候选因为同一个功能在不同版本、不同入口下有时候挂在文字属性上有时候挂在内容描述属性上多准备几个能显著提高命中率。再说位置漂移。这类浮层元素往往贴在屏幕边缘而且会随内容滚动而移动所以绝对不能缓存坐标。正确做法是每次要用的时候重新查一次拿到即用var target waitForAny(candidates, 10000); if (target) { var b target.bounds(); // 有些浮层节点的可点击区域比视觉区域小点击中心最稳 click(b.centerX(), b.centerY()); }第三是文案变化。这类元素的文字经常带倒计时或者状态词比如福袋 3福袋 2纯精确匹配会失效。所以要么用 textContains要么用正则var node textMatches(/福袋.*/).findOne(3000);还有一个很实用的技巧如果页面上同时存在多个疑似节点比如列表里每个卡片都带这个词可以通过 bounds 的区域筛选。比如只保留屏幕上半部分的或者只保留面积最大的那个通常就是当前真正生效的那个浮层。var all textContains(福袋).find(); var best null; for (var i 0; i all.length; i) { var b all[i].bounds(); if (b.top 0 b.width() 100) { if (!best || b.width() * b.height() best.bounds().width() * best.bounds().height()) { best all[i]; } } }这里必须补一句合规提醒定位能力是用来做效率提升和流程验证的不是用来批量刷取平台营销资源的。任何形式的批量操作、虚假互动都不在本文讨论范围内也建议你不要尝试收益远小于风险。3.3 让脚本稳住的三板斧随机延时、失败重试、状态日志脚本能跑通一次和能连续跑一个月是两回事。我总结下来稳定性主要靠三件事。第一件是随机延时。固定间隔的机械操作不仅容易被识别而且在真实网络环境下本来就容易撞上加载慢的情况。我习惯写一个小的延时函数function pause(min, max) { sleep(min Math.random() * (max - min)); }调用的时候写成pause(800, 1500)这样每次间隔都略有差异既贴合真实使用节奏也给界面留足了加载时间。第二件是失败重试的封装。几乎所有定位动作我都会包一层function tryFind(selector, retry, interval) { for (var i 0; i retry; i) { var node selector.findOnce(); if (node) return node; sleep(interval); } return null; }别小看这几行它把找不到就崩溃变成了找不到就多等一会儿。真实环境里一次定位失败绝大多数时候只是慢了一拍多试两次就出来了。第三件是状态日志。我建议每一步关键动作都往日志里写一行格式统一成时间 状态 结果function log(msg) { console.log([ new Date().toLocaleTimeString() ] msg); }有了日志脚本半夜停了第二天你能一眼看出是卡在哪一步。没有日志你只能靠猜。这个投入产出比高得离谱但很多新手嫌麻烦不写最后排查问题时花的時間是写日志的十倍。4. 常见问题与排查技巧实录4.1 脚本跑着跑着就停先查这三个地方这是被问得最多的问题。我的排查顺序固定是三步。第一步看无障碍服务还在不在。很多时候脚本停了是因为无障碍服务被系统悄悄关掉了。你可以在脚本主循环里定时调用auto.waitFor()或者检测当前无障碍状态一发现掉线就重新拉起或至少弹个提示。这个动作建议每几轮循环做一次成本极低。第二步看进程是不是被冻结了。判定方法很简单看日志的最后一条时间。如果日志停在一个明确的时间点而当时你正好锁屏或者切到了别的应用那基本就是后台被限制了。解决办法是逐个放开后台运行、自启动、电池优化这几项并且调试阶段保持屏幕常亮。第三步看是不是卡在某个等待上。如果一个 findOne 设了很长的超时而目标一直没出现脚本看起来就像死了。所以我从来不写无限等待所有等待都有上限超时就进入兜底分支截图存证、写日志、执行返回操作然后继续下一轮。4.2 控件定位失败的排查速查表定位失败有非常多种原因我把常遇到的整理成一张表出问题时从上往下过一遍基本能覆盖九成情况现象可能原因排查动作findOne 返回 null界面还没加载完加大超时或先等待某个必然存在的节点出现明明看得见却找不到元素在 WebView 或自绘视图里用 className 找 WebView再考虑图像方案文字匹配不上文案带了空格、换行或动态数字改用 textContains 或 textMatches 正则找到节点但 click 无效节点本身不可点击改点它的父节点或按 bounds 直接点坐标点击位置偏了存在缩放或系统适配用 bounds 实时计算别用缓存坐标同一段代码昨天能跑今天不行应用版本更新改了结构打开布局分析工具重新确认节点属性最后一行要特别说一下。做手机app自动化的人必须接受一个现实你的脚本是依附在别人界面上的对方一改版你就得跟着改。所以我写脚本时有个习惯把所有定位用的选择器集中写在一个配置对象里界面上改版只需要改这个对象不用满篇找代码var UI { entryBtn: () text(我的).findOne(3000), confirmBtn: () textContains(确定).findOne(3000), listItem: () className(android.widget.TextView).find() };这个写法一开始多花十分钟后面每次改版能省一小时。4.3 机型适配和权限的那些坑分辨率适配是最容易被低估的一环。我早期写脚本习惯用固定像素结果在一台宽屏手机上没问题换到一台小屏设备上坐标直接落到屏幕外面去了。从那以后所有坐标一律用比例表示device.width和device.height是必用的。另一个坑是状态栏和导航栏。不同设备的状态栏高度差别不小如果你用屏幕顶部往下 100 像素这种写法找顶部元素很容易点到状态栏上去。稳妥做法永远是先找到目标控件再用它的 bounds 算坐标让系统告诉你位置在哪而不是你自己估。还有一类问题是权限带来的隐蔽故障。比如存储权限没给脚本里写日志和截图就会静默失败看起来像脚本没执行实际是执行了但没留下痕迹。我的建议是脚本启动时做一次完整的自检把无障碍、悬浮窗、存储三项状态打印出来一眼就能看到缺什么。4.4 关于自动化赚收益这类说法的冷静判断网上确实有不少把手机app自动化和轻松赚收益绑定在一起的说法我个人的态度比较明确涉及批量操作、虚假互动、刷量的方向都不要碰。原因不是道德说教而是算经济账——这类操作需要不断对抗平台的风控升级投入的维护成本持续增加而一旦被判定为异常行为账号本身的价值就归零了。用技术去做一件随时可能被清零的事性价比太差。真正值得投入的方向是那些自己用、可长期、不损人的场景把每天重复的十几次操作变成一次把复杂的多步流程做成一键执行给自己做界面改版后的回归验证把手机上的机械劳动转成可复用的脚本资产。这些事情的收益不会很夸张但它是稳定累积的而且你在这个过程中练出来的定位、状态机、异常处理能力是可以迁移到其他自动化平台的。5. 脚本的可维护性与长期演进5.1 把脚本当工程项目写而不是当一次性工具写我见过太多人写完一个脚本用两天就扔原因是没法改。要想让脚本活得久有几个习惯值得养成。一是配置和逻辑分离。所有会变的东西——选择器、等待时长、循环次数、目标文案——都放到最上面的配置对象里逻辑代码只引用不硬编码。二是函数化拆分。至少拆成定位点击等待日志这几个基础函数再往上叠加业务函数。这样当你发现某类操作用了新的定位方式只需要改一个基础函数所有调用点都跟着受益。三是留一份真机截图。每次大改版调试通过后把关键界面的截图存下来附上对应的选择器。下次改版时你可以直接对照截图判断哪些节点变了比重新分析一遍快得多。5.2 从单脚本到小工具集我自己的演进路径我最初只是写了一个自动执行固定步骤的小脚本后来慢慢变成了一个小小的工具集。演进路径大概是这样先做单点任务跑通验证思路然后把公共部分抽出来做成一个基础库比如定位封装、重试封装、日志封装接着做参数化让同一份脚本能通过配置适配不同场景最后加一层简单的任务调度让多个脚本按顺序执行中间出错就跳过并记录。走到这一步之后你会发现真正有价值的东西不是脚本本身而是你在写脚本过程中形成的把重复流程抽象成状态和动作的能力。这个能力换个场景照样能用换个平台也只是语法差异。最后分享一个我个人一直在用的习惯每次脚本出问题我不只修代码还会在脚本头部加一行注释写清楚这次为什么改。几个月后回头看这些注释比代码本身更有价值因为它记录了当时的判断依据。脚本会过时判断力不会。