安卓自动化原理工程实践

安卓自动化脚本总挂怎么办?稳定性排查与修复指南(等待节点/服务自愈)

安卓自动化脚本跑着跑着就挂?本文拆解四类典型故障:空点击(元素未加载就操作)、元素定位失败(界面升级/布局变化)、无障碍服务被系统回收、分辨率与机型差异,并给出工程化解法——等待节点出现、重试机制、服务自愈重启、节点优先定位。附 12 项稳定性检查清单与代码示例,帮你把无人值守脚本跑得更稳、更省心。

约 19 分钟

无人值守的自动化脚本,最难的不是“写出来”,而是“跑得住”。本文不聊具体业务,只聊一件事:脚本为什么跑着跑着就挂?以及工程上怎么让它不挂。全文以 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 查一次节点,直到出现或达到次数上限。理解这个循环,就理解了所有等待类 API 的本质:

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 容器——旧的选择器就失配了。只赌单一属性的脚本,本质上是在赌“这个 App 永远不改版”。

解法:节点优先,且多属性组合。定位策略按稳定性排优先级:

优先级 策略 说明
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() 选子级;正则匹配——textMatchidMatchclzMatch 等容忍文案和 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 系统设置里支持配置“开机自启动”(setECSystemConfigauto_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();

坐标 vs 节点,一个表说清:

维度 写死坐标 节点定位
分辨率变化 全废 自适应(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:手机自动化 AI 智能体平台,覆盖安卓免 root、iOS 免越狱、鸿蒙 Next 三大生态,提供脚本开发、苹果群控、本地中控投屏与云控系统。→ 了解全部产品

想要真实跑起来?

本文介绍的方案均可在 EasyClick 手机自动化平台落地。官网提供完整文档、开发工具与群控云控产品,免费体验。

访问 EasyClick 官网 →