
最近在整理移动端自动化方案手头这个基于auto.js的脚本项目我前前后后折腾了两个多月中间换过好几版实现踩了无数坑才把整套流程跑顺。这篇文章就是要把这个项目完整地拆开来讲从为什么选auto.js开始到环境怎么搭、脚本怎么写、打包怎么出再到线上跑的时候出过的各种幺蛾子全部给你捋一遍。先交代一下背景。我手上有一批安卓设备日常要做重复性的操作批量打卡、自动化回归测试、定时清理数据、多App间的数据搬运。手动点来点去不仅浪费时间还容易出错。后来我盯上了auto.js它直接用JavaScript就能调用安卓系统的无障碍服务来模拟点击、滑动、输入不需要root对普通开发者非常友好。如果你也想给安卓手机写自动化脚本或者你正准备做App自动化测试这篇文章应该能帮你省下不少试错成本。需要说明的是我做的这些都是正向的效率工具场景尽量避免去做任何违反平台规则、影响他人权益的事情脚本的能量用在正经地方还挺好用的。1. 内容整体设计与思路拆解1.1 为什么偏偏选auto.js做自动化安卓端做自动化方案其实不少我当时列过一个对比清单把几个主流路子的优劣势都过了一遍。Appium走的是WebDriver协议功能很全跨平台、支持多语言但它需要电脑连着手机跑而且环境配置很重。对一批设备做轻量级操作Appium显得大材小用光是启动server、初始化session就得半天。Tasker、MacroDroid这类工具强在“触发器动作”的规则组合但它追求的是零代码逻辑稍微复杂一点就写不下去你想做循环、异常重试、动态解析文本基本没门。auto.js的优势恰恰就在这里它把无障碍服务的接口封装成了JS函数你在手机上直接写脚本、直接跑不需要电脑也能干活。而且它保留了完整的编程能力——循环、条件、正则、多线程、HTTP请求、文件读写都能用。对我来说它更像一个“带着触手”的Node.js环境只不过触手长在了安卓系统上。这个项目在设计之初定了几条原则能用控件定位就坚决不用坐标因为分辨率不同、屏幕不同坐标一换设备就废脚本必须带日志和异常捕获不能静默死掉所有涉及网络请求的部分要有超时和重试机制移动网络不稳定是常态。这套思路后来帮我少踩了很多坑。1.2 项目整体模块划分整个项目我按功能分成四块核心调度模块、交互模块、任务模块和工具模块。核心调度模块负责脚本的启动入口、退出条件、主循环逻辑。交互模块封装了点击、滑动、输入、等待控件等基础操作对外提供统一API。任务模块是具体业务逻辑的集合比如“打开AppA完成打卡”“进入AppB抓取数据”“清理应用缓存”等等。工具模块处理一些杂活比如日志写入、时间格式化、正则匹配、HTTP请求封装。分层的好处是一个任务挂了不影响其他任务而且新增一个自动化任务只需要新写一个任务函数不用动底层框架。项目到后期脚本数量从最初3个增加到20多个如果没有这套分层维护成本会高到我不想再打开编辑器。2. 环境搭建与工程配置细节2.1 版本选择Auto.js V4还是AutoX.js这个我得单独拎出来说因为版本选错真的会让你怀疑人生。老牌的Auto.js在V4版本停更了很多设备上会出现兼容性问题。后来社区里有人维护了AutoX.js这个分支修复了大量机型兼容bugAPI基本兼容老版本目前我用下来是最稳的。如果你的项目是新建的直接用它就好。V4和AutoX.js在无障碍服务、悬浮窗、打包APK这些核心功能上都能用但AutoX.js对Android 10以上系统适配更好尤其是对分区存储、前台服务限制这些新特性做了处理。真要说用户量的话AutoX.js相关的社群活跃度明显更高遇到问题搜一下基本都能找到答案。安装方式不复杂手机浏览器直接搜AutoX.js进到GitHub发布页下载对应APK装好就行。安装后先不要急着写脚本把“无障碍服务”“悬浮窗”两个权限先给到否则脚本跑不起来。2.2 手机端预算环境和权限配置拿到App之后第一件事是把权限体系捋清楚。无障碍服务是整个自动化的基础脚本里的click、scroll、text这些操作全靠它。App引导页通常有一键配置点击后会跳转到系统无障碍设置界面找到对应的服务名打开开关。这里有个常见的坑部分国产ROM会在后台自动回收无障碍服务比如MIUI、ColorOS需要在电池优化里把AutoX.js设为“无限制”否则脚本跑一半就失灵。悬浮窗权限也很重要。脚本运行状态、日志预览、中止按钮都是通过悬浮窗展示的没有悬浮窗权限你连脚本什么时候卡死了都不知道。在系统设置里搜索“悬浮窗”把AutoX.js允许开关打开即可。如果你要执行input操作还需要在系统设置里打开“USB调试安全设置”。这个选项藏得比较深一般在开发者选项里允许模拟点击开关。注意部分手机默认隐藏了这个选项得连续点击版本号进入开发者模式之后才能看到。2.3 电脑端开发调试配置很多人不知道auto.js不只是手机端App它还有一个桌面端配套工具叫Visual Studio Code插件。插件安装好之后手机和电脑连同一个Wi-Fi用USB数据线连接或者扫描二维码就能互相通信。我日常的流程是电脑端写好脚本并保存右键“发送到手机”手机端AutoX.js自动接收然后在电脑端开着日志面板手机端跑一遍脚本日志实时同步到电脑调试效率比在手机上改代码高非常多。工程目录我习惯这样组织project/ ├── core/ # 核心模块入口、调度、配置 │ ├── main.js │ └── config.js ├── modules/ # 交互模块封装 │ ├── uiautomator.js │ ├── gesture.js │ └── network.js ├── tasks/ # 具体业务任务 │ ├── checkin.js │ ├── batch_clear.js │ └── ... ├── utils/ # 工具函数 │ ├── logger.js │ └── auto_retry.js └── project.json # 项目配置project.json文件记录了包名、版本号和入口文件打包APK的时候会用到。这个结构不是硬性要求但强烈建议按功能分文件不然一个脚本写到两三千行后面找bug能把眼睛看花。2.4 无障碍服务的判断与自启逻辑脚本编写过程中有一个基础问题是新手一定会碰到的无障碍服务没开脚本跑起来后什么反应都没有。所以在每个脚本开头我都要做一次服务可用性判断。auto.js里提供了一个方法auto()。不带参数调用它如果无障碍服务未开启会直接跳转到设置页提醒用户开启服务已开启就直接继续执行。另外还有auto.waitFor()它不会跳设置页而是等在那里直到服务可用再往下走。我通常会在每个任务入口加一个检查函数function ensureService() { if (!auto.service) { toast(请先开启无障碍服务); auto.waitFor(10000); if (!auto.service) { exit(); } } }这里有个细节值得注意auto.service这个属性在无障碍服务异常断开后会变成null所以不仅仅是首次启动要校验每次任务开始前都应该校验一遍。我在批处理任务中就是这么干的十个任务跑下来即使中途有个任务把服务弄挂了下一个任务开头也能自动检测出来并引导修复不会整个批处理直接报废。3. 核心脚本逻辑与API实战3.1 坐标系、控件选择器与package id写auto.js脚本最先要搞明白的就是怎么“告诉手机点哪里”。坦白说Auto.js的选择器系统是我见过所有安卓自动化框架里最顺手的一层封装。基于控件定位的写法长这样// 等一个文本为“签到”的按钮出现然后点击 text(签到).waitFor(); text(签到).click();如果界面上有多个相同文本的控件你可以用desc、className、packageName、id等条件组合精确锁定// 通过控件id定位 id(btn_confirm).findOne().click(); // 通过文本类名组合定位 text(确认).className(android.widget.Button).findOne().click();坐标定位就简单粗暴了直接click(x, y)。但坐标定位的致命弱点是跟分辨率强相关同一个坐标在不同设备上可能点的是完全不同的位置。我的原则是能控件定位就不坐标定位控件定位实在找不到才用坐标轮廓加找色辅助的方式兜底。控件选择器还有一个极其实用的能力findOnce、findAndWaitFor、exists等。初期写的时候多读两遍API文档你会发现很多逻辑可以用一行代码替代原来的十行循环代码少bug就少。3.2 高频交互API点击、滑动、输入点击类的API除了click()还有一个press(x, y, duration)可以做长按操作。长按在某些场景下非常有用比如重命名桌面图标、触发App的隐藏菜单。滑动类的高频API是swipe(x1, y1, x2, y2, duration)。duration是滑动持续时间单位毫秒。很多人忽略了这个参数的实际意义快速滑动和慢速滑动产生的效果差别很大。比如在长列表中定位某个条目慢速滑动更容易被系统识别为“人工操作”在某些有风控的App里不容易触发验证。手势方面gesture()可以模拟多段滑动路径和曲线手势用来自定义解锁图案、绘制签名什么的。它接收一组表示时长的参数和一组坐标序列// 模拟向右滑动解锁 gesture(500, [200, 800], [900, 800]);输入操作把焦点放到输入框之后用setText()写入内容。注意setText()需要目标输入框处于聚焦状态否则可能无效。有些App自定义了输入框控件setText()也未必完全兼容这时候可以用剪贴板方式setClip(text)然后长按输入框点“粘贴”。这个方法土但极稳我在跨App数据搬运场景中一直在用。3.3 等待机制与空指针防护写自动化脚本时最忌脚本跑得太快。界面还没加载完脚本已经执行到点击了控件找不到程序直接抛异常。auto.js里提供了sleep(ms)、waitFor()、findOne(timeout)等机制。我的习惯是凡是要点击的控件一律用findOne(timeout)而不是findOne()因为findOne()是无限等待一旦界面上永远不出这个控件脚本就永久卡死了。类似地waitFor()也一定要带上超时时间。举一个实战例子// 设置超时10秒超过就放弃本次操作并返回null方便上层处理 var btn text(立即领取).findOne(10000); if (btn) { btn.click(); } else { log(未找到领取按钮跳过); }很多初学者写的脚本容易漏掉null判断结果就是一遍一遍报错。其实加上一个空对象判断并不费事但对脚本稳定性的提升是质的区别。3.4 定时、线程与多任务调度auto.js既然是一门编程语言环境那就绕不开线程和定时任务。要做一个每天定时执行的打卡脚本只需要用setInterval或者写一个循环判断时间即可。如果是“每天执行一次”这种需求我更推荐在脚本内部做日期判断而不是依赖系统定时器反复跑while (true) { var today new Date().toDateString(); if (lastRunDate ! today isInTimeWindow(new Date())) { doTask(); lastRunDate today; } sleep(60 * 1000); // 每分钟检查一次 }多线程场景主要用在“一个线程跑主流程一个线程监控异常并重启服务”。Auto.js的threads.start()可以启动新线程运行时给悬浮窗增加一个停止按钮也常常用线程实现。注意cell操作UI的函数只能在UI线程调用纯粹的后台任务可以放心用子线程但如果子线程里要操作界面控件需要使用ui.run()包一层。3.5 图片识别与找色补位控件定位虽然稳但偶尔也会遇到控件信息被App隐藏的情况。比如一些App把按钮渲染成一张图片或者使用Flutter、Unity绘制界面控件树里拿不到有效节点。这时候就要靠图片识别来补位。auto.js内置了images模块支持findColor、findImage等找色找图函数。举个例子我要在屏幕上找一个固定图案的“开始”按钮可以先截屏再用目标小图在截屏大图里匹配var big images.captureScreen(); var small images.read(/sdcard/start_btn.png); var res images.findImage(big, small, { threshold: 0.8, region: [0, 0, 1080, 1920] }); if (res) { click(res.x, res.y); }这个方案需要提前准备好模板图片而且不同分辨率的设备需要不同模板。我的做法是在配置里按设备型号保存模板路径批处理时根据当前设备的型号自动选择对应图片。4. 一个完整实操案例4.1 需求描述与流程拆解光讲理论不落地等于白写我拿一个实际运行了很久的脚本当案例来讲一遍完整流程这个任务叫“多App批量签到”。需求是这样的每天早上 9 点我需要在五个App里分别完成签到每个App的签到按钮位置不同、界面风格不同有些是文字按钮有些是图片按钮还有一个是WebView渲染的按钮。流程拆解下来长这样按顺序依次打开目标App等待App首页加载完成进入签到页面入口可能是首页banner、个人中心、弹窗等找到签到按钮并点击判断签到是否成功根据弹窗文案、按钮状态变化等记录结果关闭当前App启动下一个这六个步骤看起来简单实际写下来两百多行代码跑不掉但每一行都有明确的目的。4.2 脚本运行实现与关键代码整个脚本的入口比较简洁核心逻辑集中在了“签到一次”这个函数里// 签到主函数appName表示目标App的唯一标识 function signIn(appConfig) { var result { app: appConfig.name, success: false, reason: }; try { app.launch(appConfig.packageName); // 等待首页关键控件出现 var homeReady desc(appConfig.homeDesc).findOne(10000); if (!homeReady) { result.reason 首页加载超时; return result; } // 进入签到入口 clickByConfig(appConfig.entry); // 执行签到 clickByConfig(appConfig.signBtn); // 判断结果 sleep(1500); if (text(appConfig.successWord).findOne(2000)) { result.success true; } else if (text(appConfig.failWord).findOne(2000)) { result.reason appConfig.failWord; } else { result.reason 未知结果; } } catch (e) { result.reason e.message; } return result; }clickByConfig是我封装的一个函数入参是一个配置对象根据对象里的type字段决定走控件定位还是坐标定位。好处是新增一个App的时候只需要在配置文件里加一段JSON不需要改主逻辑。App配置长这样[ { name: 天气App, packageName: com.example.weather, homeDesc: 首页, entry: { type: text, value: 我的 }, signBtn: { type: desc, value: 签到 }, successWord: 已签到, failWord: 签到失败 }, { name: 社区App, packageName: com.example.community, homeDesc: 首页, entry: { type: text, value: 积分中心 }, signBtn: { type: id, value: sign_button }, successWord: 今日已签, failWord: 网络异常 } ]主流程里加了一个指数退避重试机制连续两次失败就不再重试避免因为网络问题反复撞同一个App导致被风控。这种细节在单机手动操作时根本不会被注意到但放到自动化场景里不加控制就会变成灾难。4.3 运行结果与日志分析每次签到任务跑完之后脚本会把结果写到本地日志文件同时通过请求的方式上报到我的电脑端方便我第二天早上快速巡检。日志内容大概长这样[09:00:01] 开始批量签到任务 [09:00:03] 天气App 启动成功 [09:00:06] 天气App 签到成功 [09:00:08] 社区App 启动成功 [09:00:15] 社区App 签到失败: 网络异常等待重试 [09:00:22] 社区App 重试成功 [09:00:25] 全部任务执行完毕共签到4个失败0个这个脚本稳定跑了大半年成功率基本在98%以上剩余的2%基本都是网络波动或App改版导致控件路径变化。整体效果我很满意至少每天给我省出了十五分钟的重复劳动时间。5. 常见问题与排查技巧实录5.1 无障碍服务频繁丢失这个问题在国产ROM上格外明显。表现是脚本跑着跑着toast弹一下“服务已断开”然后就再也点不动了。排查思路是先看电池优化白名单把AutoX.js加入“无限制”。再看自启动管理很多ROM会杀掉后台进程不小心就把它识别成了“长期后台耗电应用”。如果你用的是小米系、华为系、OPPO系手机建议把“后台弹出界面”“常驻通知”“锁屏后保持运行”这些开关全打开。如果还不行写一个守护线程监控auto.service的状态一旦发现服务断开就尝试重新绑定。注意重新绑定需要用户在设置界面手动开启没法在脚本内直接打开。所以我的做法是弹窗提示用户并且打开系统设置页引导用户操作。5.2 控件定位找不到这是初学者最崩溃的问题辛辛苦苦写的text(签到).click()运行时日志告诉你“null”。大概率原因是App界面里确实有这个按钮但它不是一个文本控件而是一张图片或者是画布绘制出来的。这时候可以先打开App自带的“布局分析”工具查看屏幕的控件层级确认目标节点的属性。如果节点里确实没有文本信息再走找色或者模板匹配的路线。另一个原因可能是页面还没渲染完成。我遇到过很多次点进去之后控件树已经加载但视觉上按钮还在渐变出现这时候加一个sleep(800)或者用waitFor加超时就能解决。5.3 脚本运行慢与资源占用高如果脚本长时间循环执行尤其是每轮都截屏、找色、找图CPU和内存占用会直线上升手机会发热。优化手段主要有三个。第一降低轮询频率能500毫秒一查就不要100毫秒一查。第二截屏之后及时回收图片对象用recycle()释放内存不然几百张截屏累积下来内存直接爆掉。第三用控件定位替代找图找色控件定位的消耗远低于图像匹配。5.4 打包APK后无法运行脚本调试没问题打包成独立APK反而出问题这事情我遇到过好几次典型的坑有两个。第一个是打包时勾选了“使用系统签名”但设备没root导致部分接口权限不足。第二个是打包APK没有声明需要的权限比如网络权限、悬浮窗权限。在AutoX.js打包界面的权限列表里把用到的权限全部勾上再重新打一遍基本能解决。另外APK打包后的无障碍服务名称跟开发版不一样需要重新在系统设置里开启一次。脚本内部可以做一个检测如果服务没开自动跳转设置页。6. 脚本开发中的工程化心得6.1 写脚本也要讲代码洁癖很多人觉得脚本是一次性工程能跑就行不用管代码质量。我前半年的心态也一样直到脚本量起来之后改一个公共函数要打开几十个文件逐个同步才开始后悔没有早点做抽象。现在我的做法是公共逻辑全部收拢到modules目录下任务文件里只写业务步骤所有关键节点都预留日志输出配置文件独立于代码修改配置不碰代码。这套规矩听起来极其朴素但对长期维护的帮助是巨大的。哪怕这个脚本三个月没动回头再看也能在五分钟内定位到需要修改的位置。6.2 脚本的合规使用与边界意识这部分我必须专门加一个小节。auto.js的能力很强但能力越强越要清楚边界在哪里。我个人的原则是只用于提升自己的设备使用效率、个人自动化测试、数据整理等合规场景。不要用它去破解、抢购、批量注册、刷量也不要干扰平台正常秩序。脚本开发是一种效率技能技能本身没有原罪但使用方法直接决定了它会带来正面价值还是负面后果。这一点在实际动手之前就想清楚能帮你少走很多弯路。6.3 后续可以怎么扩展这个项目稳定运行后我还在断断续续往里加东西。目前正在做的方向是接入更复杂的企业微信机器人通知把脚本执行结果推送到群内另一个方向是给脚本增加简单的Web管理后台这样我不用打开手机也能查看所有设备上脚本的运行状态。如果你也打算深入这块建议优先把“可控性”和“可观测性”做起来也就是随时能看日志、随时能停任务、出问题能快速定位。这两个点做扎实了脚本数量再多也不会失控。最后分享一个我一直在用的习惯写脚本时主动给每个关键步骤打点把日志、执行时间、执行结果写全。刚开始会觉得麻烦但等脚本真的出了bug你看着完整日志三分钟就能定位问题的时候就会感谢当时那个多写了几行log的自己。