安卓自动化就是用程序替掉重复的手工操作。主流走免 root 路线,靠无障碍服务和 ADB 两条通道。
四类典型故障
无人值守脚本写出来之后,难的是让它一直跑。这篇不讲具体业务,讲脚本为什么跑着跑着就停,以及怎么处理。例子用 EasyClick,它是安卓免 root 自动化平台,脚本语言是 JavaScript(官方文档)。文里的函数名都在公开文档里,可以对照。
脚本停掉的样子很多:日志停在某一行、界面停在某一页、悬浮窗消失、设备黑屏。大体是后面四类:
| 故障类型 | 现场表现 | 一句话根因 |
|---|---|---|
| 空点击 | 日志显示“点了”,界面却没反应,任务卡在下一步 | 元素还没出现/还没可点,脚本就先动手了 |
| 定位失败 | 选择器反复匹配不到节点,同一个地方反复超时 | 界面升级、布局调整、文案变化,旧定位条件失效 |
| 服务被回收 | 无障碍服务掉线,节点、点击、输入全部失灵 | 内存紧张或厂商省电策略,回收了后台服务 |
| 环境变化 | 同一个脚本 A 机型正常、B 机型乱点 | 分辨率/DPI/显示缩放差异,写死的坐标失效 |
脚本往往默认界面是稳的、服务在线、坐标不变。手机上这三样都会变。每一条都要有检测,变了还要能恢复。
空点击是元素还没出现就操作。人是看到按钮才点,脚本不知道页面加载到哪一步。常见写法是 sleep(3000) 之后直接点。网络慢一点、多一个广告位、动画多播半秒,元素还没出来,点击落在空白处。这种点击不报错,只是点了没反应。录制出来的脚本能跑、但不稳,经常是这个原因。把固定秒数换成等元素出现。EasyClick 的选择器有 waitExistNode(毫秒),最多等 N 毫秒,元素一出现就返回,超时返回空(选择器与节点文档):
function main() {
// 等待"立即领取"出现,最多等 5 秒;出现即返回,不用干等
let node = text("立即领取").waitExistNode(5000);
if (node) {
node.click(); // 节点区域随机点击
} else {
loge("等待超时:5 秒内未出现目标节点");
}
}
main();
也可以自己轮询,不用现成的等待函数。每 500ms 查一次节点,直到出现,或者达到次数上限:
function main() {
let node = null;
// 轮询代替固定 sleep:每 500ms 查一次,最多查 10 次
for (let i = 0; i < 10; i++) {
node = text("立即领取").getOneNodeInfo(0); // 0 表示不等待,立即查一次
if (node) {
break;
}
sleep(500);
}
if (node) {
node.click();
} else {
loge("轮询 10 次仍未找到节点");
}
}
main();
两种方式对比:
| 写法 | 行为 | 问题 |
|---|---|---|
sleep(3000) 后直接操作 |
固定等 3 秒 | 等不够(元素没出)或白等(浪费时间) |
waitExistNode(5000) |
等到出现为止,最多 5 秒 | 需要节点体系可用(无障碍/代理模式全功能) |
轮询 getOneNodeInfo(0) + sleep(500) |
每 500ms 查一次,最多 N 次 | 适合自己控制节奏,原理与 waitExistNode 相同 |
两个补充细节。判断有没有用 has(),等待出现用 waitExistNode(),各有分工。拿到的节点在页面重绘后可能失效,用节点的 isValid() 判断,失效就重新取。
定位失败多半出在界面升级和布局变化。选择器匹配的是属性,属性会变。App 发版改了资源 id、改了按钮文案,或者把控件从 A 容器挪进 B 容器,旧的选择器就对不上。只匹配一个属性,一改版就失配。优先用节点,并且把多个属性叠在一起。按稳定性,可以这样排:
| 优先级 | 策略 | 说明 |
|---|---|---|
| 1 | id(资源 ID) | 最稳定,但混淆或重构后可能变化 |
| 2 | 文本 text / 描述 desc | 语义稳定;文案一变就失效,用 textMatch/descMatch 正则兜底 |
| 3 | 多属性组合 + 级联 | 同时匹配文本、类名、选中态等;或先选父级再选子级 |
| 4 | 坐标兜底 | 只在节点体系拿不到时用,配合节点区域随机点击 |
多属性组合的例子,同时约束“文本 + 类名 + 选中状态”,比单一属性鲁棒得多:
function main() {
// 多属性组合:文本(正则) + 选中态 + 类名,三者同时满足
let selector = textMatch(".*选择器.*")
.checked(true)
.clz("android.widget.CheckBox");
let node = selector.getOneNodeInfo(3000);
if (node) {
node.click();
} else {
loge("主策略定位失败,进入备选策略");
}
}
main();
选不中目标时,先选父级,再用 .child() 选子级。textMatch、idMatch、clzMatch 用正则,文案和 ID 有小变化也能对上。发布前在开发工具的节点面板里把每页的定位条件对一遍,比上线后对着日志猜省时间。界面会升级,定位失效按一定会发生来写。
服务回收与机型差异
无障碍服务会被系统回收。内存紧张时,安卓会收后台服务。国产厂商的省电管理、神隐模式也会收。官方常见问题里有一条:脚本跑了大约 20 分钟自己停了,悬浮窗也没了,原因是神隐模式或省电模式(常见问题文档)。服务被收掉之后,节点、点击、输入都失效。脚本进程还在,只是什么都不做了。检测和拉起靠三个全局函数(全局模块文档):
| 函数 | 作用 |
|---|---|
isServiceOk() |
自动化服务是否正常,返回 true/false |
startEnv() |
启动自动化服务环境 |
daemonEnv(true) |
守护自动化环境,尽量保证自动服务不掉线(EC 6.7.0+) |
官方文档(AI 辅助编程文档)里有一个 autoServiceStart 模板,顺序是检测、启动、再检测,可以放在脚本入口:
// 官方 autoServiceStart 模板:循环检测,服务不在线就启动,最多 time 次
function autoServiceStart(time) {
for (let i = 0; i < time; i++) {
if (isServiceOk()) {
return true;
}
startEnv();
sleep(1000);
}
return isServiceOk();
}
function main() {
// 入口先确保服务就绪,3 次机会
if (!autoServiceStart(3)) {
loge("服务启动失败");
return;
}
// 业务逻辑……
}
main();
也可以监听服务被回收的事件,自动拉起。全局模块能监听无障碍服务被销毁、被中断两个事件(acc-service-destroy / acc-service-interrupt)。收到就再跑一次自愈:
function main() {
autoServiceStart(3);
// 服务被系统销毁/中断时,自动尝试恢复
observeEvent("acc-service-destroy", function (key, data) {
loge("无障碍服务被销毁,开始自愈: " + data);
autoServiceStart(3);
});
observeEvent("acc-service-interrupt", function (key, data) {
loge("无障碍服务被中断,开始自愈: " + data);
autoServiceStart(3);
});
// 业务逻辑……
}
main();
设备上也要放行。把 EC 加进厂商的后台白名单,关掉省电和神隐限制。EC 系统设置里可以开开机自启动(setECSystemConfig 的 auto_start_service)。重启之后脚本能自己起来。只在脚本里自愈,或者只在手机上放行,都跑不久。
分辨率和机型也会让脚本点偏。物理分辨率、DPI、刘海和挖孔、显示缩放都不一样,同一个 (x, y) 在两台手机上不是同一个位置。EasyClick 支持安卓 5.0 到目前的系统,机型一多,写死坐标就更容易错。用节点定位。节点的 bounds 是系统按当前屏幕算的,换机型、改分辨率不用改脚本。节点的 click() 点在节点区域里,每次位置不完全一样,不用自己算坐标。滑动、手势这些必须用坐标的操作,按屏幕宽高的比例来写,不要写死像素:
function main() {
let w = device.getScreenWidth();
let h = device.getScreenHeight();
// 从屏幕 80% 高度滑到 20% 高度,换机型也能用
swipeToPoint(w / 2, h * 0.8, w / 2, h * 0.2, 500);
}
main();
写死坐标和节点定位的差别:
| 维度 | 写死坐标 | 节点定位 |
|---|---|---|
| 分辨率变化 | 全废 | 自适应(bounds 按当前屏幕计算) |
| 机型差异 | 每台机器一套坐标 | 一套脚本通吃 |
| 布局微调 | 偏一点就点错 | 节点还在就点得准 |
| 操作特征 | 每次都同一个像素点 | 节点区域内随机,位置不完全一致 |
节点拿得到就用节点。坐标留到节点体系拿不到的时候。
重试、超时、日志和截图
前面四类是事先防住。重试、超时、日志、截图是失败之后怎么处理。关键步骤外面再包一层重试,失败就隔一段时间再试,到了次数上限走失败分支:
// 通用重试:fn 返回真值即成功,最多重试 times 次,每次间隔 1 秒
function retry(times, fn) {
for (let i = 1; i <= times; i++) {
try {
let result = fn();
if (result) {
return result;
}
} catch (e) {
loge("第 " + i + " 次执行异常: " + e);
}
sleep(1000);
}
return null;
}
function main() {
let node = retry(3, function () {
return text("立即领取").waitExistNode(3000);
});
if (node) {
node.click();
} else {
loge("重试 3 次仍失败,进入失败处理流程");
}
}
main();
等待和重试都要设上限。waitExistNode(5000) 的超时、重试次数,都是不让脚本一直等下去。日志用 setSaveLogEx(true, "/sdcard/aaa/", 1024 * 1024, "testlog") 写到磁盘,失败分支用 loge 记下现场,再注册 setExceptionCallback 接脚本异常停止。失败时把屏幕截图存到本地,和日志对着看,一般几分钟能看出停在哪一步。没人看着的时候,用 sendDingDingMsg(url, secret, msg, atMobile, atAll) 把失败消息发到钉钉群。当场恢复不了,用 restartScript(null, true, 3) 重启脚本。文档特别提醒,这个方法威力巨大,要不要自动重启得自己控制好。
发布前的稳定性检查清单
发布前核对这些项:
| 检查项 | 要求 | 对应章节 |
|---|---|---|
| 每个操作前是否等待了节点 | 用 waitExistNode / getOneNodeInfo(超时),不用裸 sleep | 二 |
| 是否有固定 sleep 依赖 | 仅用于模拟人为节奏 | 二 |
| 定位是否单一属性 | 至少双属性组合,必要时正则/级联 | 三 |
| 是否处理了节点为空的返回值 | 每个 node 判空,空则走降级或失败分支 | 三 |
| 入口是否检测服务状态 | autoServiceStart 循环启动 + 校验 | 四 |
| 是否配置服务自愈 | observeEvent 监听 acc-service-destroy / acc-service-interrupt | 四 |
| 设备侧是否放行后台 | 关闭省电/神隐限制,加入后台白名单,配置开机自启 | 四 |
| 是否有写死坐标 | 全部换成节点定位或相对坐标 | 五 |
| 是否有多机型验证 | 主流分辨率与系统版本各过一遍 | 五 |
| 所有等待与重试是否有上限 | 超时参数、次数上限齐全 | 六 |
| 是否留日志与截图 | setSaveLogEx + 失败分支截图 | 六 |
| 是否配置异常回调与告警 | setExceptionCallback + sendDingDingMsg | 六 |
FAQ
Q1:脚本跑十几分钟就自己停了,悬浮窗也没了,是什么原因?
A:最常见是厂商的省电管理/神隐模式在后台杀进程(vivo 手机管家的省电管理、小米的神隐模式都干过这事),再就是内存紧张时系统回收了无障碍服务。对策分两层:设备侧把 EC 加入后台白名单、关闭省电限制;脚本侧用 isServiceOk() 检测 + startEnv() 自动重启做自愈。
Q2:waitExistNode 和固定 sleep 有什么区别?
A:sleep(3000) 是“不管元素出没出来,都等 3 秒”;waitExistNode(5000) 是“等到元素出现为止,最多等 5 秒”,元素一到立即继续。固定等待要么等不够、要么白等,等待类操作应优先用 waitExistNode。
Q3:界面升级后脚本找不到控件了,怎么办? A:先做“抗升级”定位:多属性组合(id + 文本 + 类名)而不是单一属性;文案可能变的地方用 textMatch 正则;父级结构变化用级联选择(先选父再选子)。最后保留坐标兜底并记日志,升级后对照日志快速定位是哪一级定位失效。
Q4:无障碍服务被系统回收后,脚本能自动恢复吗?
A:默认不能,需要脚本自愈:isServiceOk() 检测 + startEnv() 重启,官方 autoServiceStart 模板就是“检测-启动-再检测”的循环;还可以监听 acc-service-destroy / acc-service-interrupt 事件,被回收时自动拉起。
Q5:同一个脚本在不同手机上表现不一样,怎么处理? A:优先节点定位,天然适配分辨率差异;必须用坐标时按屏幕宽高比例换算,不写死像素;发布前在主流分辨率与机型上过一遍稳定性清单。
Q6:脚本挂掉之后,怎么复盘是哪一步出的问题? A:靠“留证”:setSaveLogEx 落盘日志、setExceptionCallback 记录异常停止、失败分支截图存本地、必要时 sendDingDingMsg 发告警。日志 + 截图对照,很快就能定位到挂掉的那一步。
Q7:固定 sleep 是不是完全不能用了? A:不是。等待元素出现优先 waitExistNode;sleep 适合“人为节奏”场景,比如两次点击之间模拟人工操作间隔。原则是:能用“条件等待”就不用“时间等待”。
Q8:脚本卡在某个等待上不动了怎么办? A:所有等待都要有上限:waitExistNode 传超时毫秒、重试循环设次数上限;配合 setExceptionCallback 捕获异常,必要时 restartScript 自动重启脚本。无人值守建议加运行监控,超时未完成自动拉起。
Q9:为什么点击要在节点区域内随机? A:一是坐标自适应。节点区域由系统按当前屏幕计算,换机型不用改脚本;二是降低操作特征。每次点击位置不完全一致,避免“每次都点同一个像素点”的机械行为。
Q10:稳定性检查和功能测试有什么不同? A:功能测试验证“能不能跑通一条流程”;稳定性检查验证“长时间无人值守跑会不会挂”,重点覆盖等待、超时、服务回收、异常分支、日志留证这些工程化项,是发布前的最后一道关。
关于 EasyClick:手机自动化脚本开发平台,覆盖安卓免 ROOT、iOS 免越狱、鸿蒙三大手机生态,还提供苹果脚本编写、苹果群控、安卓群控、远程云控、AI 群控手机等。→ 了解全部产品
想要真实跑起来?
本文介绍的方案均可基于 EasyClick 能力在 iEasyClick 落地。官网提供完整文档、开发工具与自动化产品,免费体验。