EasyClick 入门指南(九):异常处理与脚本健壮性 ------ 从"不堪一击"到"金刚不坏"
专栏:《EasyClick 自动化测试从入门到实战》
适用:Android / iOS 7.1.0+ / 鸿蒙 Next
核心痛点:为什么脚本跑着跑着就崩了?为什么遇到弹窗就傻眼了?
前言:异常是脚本的"阿喀琉斯之踵"
在自动化脚本的世界里有一句名言:
没有异常处理的脚本,就是一段定时炸弹。
无论是节点找不到、网络超时,还是突如其来的系统弹窗,任何一个未捕获的异常都可能导致脚本崩溃。本期我们将基于官方文档,教你编写工业级的异常处理逻辑。
一、核心武器:try...catch...finally
这是 JavaScript 处理异常的基石。记住一条铁律:finally是用来保命的。
js
function riskyOperation() {
let node = null;
try {
// 1. 尝试执行可能出错的代码
node = text("不存在的按钮").getOneNodeInfo(1000);
node.click(); // 如果 node 是 null,这里必崩
} catch (e) {
// 2. 捕获错误,防止脚本爆炸
loge("捕获到异常: " + e.message);
// e.stack 可以查看详细堆栈
}
}
二、常见异常场景与官方解决方案
1. 节点操作异常(最常见)
场景 :getOneNodeInfo返回 null,却直接调用 node.click()。
js
function safeClick(selector) {
let node = null;
try {
node = selector.getOneNodeInfo(1000);
if (!node) throw new Error("Node not found");
lockNode(node); // 锁定防止失效
node.click();
return true;
} catch (e) {
logw("点击失败: " + e.message);
return false;
} finally {
// 确保锁被释放,防止内存泄漏
if (node) releaseNode(node);
}
}
2. 图色资源未回收(OOM 杀手)
场景 :截图后忘记 recycle,导致内存溢出(OOM)。
js
function safeFindImage(template) {
let screen = null;
try {
screen = image.captureScreen();
if (!screen) throw new Error("Capture failed");
return image.findImageEx(screen, template, {});
} catch (e) {
loge("找图异常: " + e.message);
return null;
} finally {
// finally 是资源回收的关键
if (screen) {
image.recycle(screen);
logd("截图资源已回收");
}
}
}
3. OCR 初始化失败
场景 :initOcr返回 false,后续调用崩溃。
js
let ocrInst = null;
function initOCR() {
try {
ocrInst = ocr.newOcr();
let ok = ocrInst.initOcr({type: "ppocr"});
if (!ok) throw new Error("OCR init failed: " + ocrInst.getErrorMsg());
logd("OCR 初始化成功");
} catch (e) {
loge(e.message);
exit(); // 初始化失败,终止脚本
}
}
三、全局异常捕获
setExceptionCallback(最后的防线)
这是脚本崩溃前的最后机会,用于兜底处理和资源清理。
js
function main() {
// 必须在 main 最开始注册
setExceptionCallback(function (msg) {
loge("!!! 脚本异常停止 !!!");
loge("异常信息: " + msg);
// 1. 保存案发现场
screencap("/sdcard/exception_" + Date.now() + ".png");
// 2. 释放全局资源
if (ocrInst) {
ocrInst.releaseAll();
}
// 3. 可选:重启逻辑
// startOtherScript("main.js");
});
// ===== 业务脚本 =====
logd("脚本启动...");
text("肯定会崩的按钮").click(); // 模拟崩溃
}
main();
📌 配合 setStopCallback(双保险)
区分正常停止 和异常崩溃。
js
// 正常停止(用户点击停止/exit)
setStopCallback(function () {
logi("脚本正常停止,释放资源...");
});
// 异常停止(崩溃/报错)
setExceptionCallback(function (msg) {
loge("脚本异常停止: " + msg);
});
| 回调 | 触发场景 | 用途 |
|---|---|---|
setStopCallback |
手动停止 / 正常退出 | 常规清理 |
setExceptionCallback |
JS 运行时错误 / 崩溃 | 抢救数据 & 报错 |
四、实战:通用"弹窗杀手"
场景:应对"更新提示"、"权限申请"或"广告"。
js
function killPopup() {
logd("启动弹窗杀手...");
const selectors = [
text("以后再说"), text("取消"), text("忽略"),
desc("关闭广告"), label("Skip")
];
// 限制次数,防止死循环导致系统判定假死
for (let i = 0; i < 10; i++) {
let killed = false;
for (let sel of selectors) {
try {
if (sel.exists() && sel.click()) {
logw("干掉弹窗: " + sel);
killed = true;
sleep(800);
break; // 重新开始扫描
}
} catch (e) { /* 忽略单个选择器错误 */ }
}
if (!killed) break; // 没杀到就退出
}
logd("弹窗杀手任务完成");
}
五、进阶:重试机制(Retry)
场景:网络抖动或页面加载慢导致点击失败。
javascript
function retryClick(selector, maxRetry = 3) {
for (let i = 0; i < maxRetry; i++) {
try {
logd(`尝试第 ${i + 1} 次点击...`);
if (selector.click()) return true;
} catch (e) {
logw("点击异常,准备重试: " + e.message);
}
sleep(1500);
}
loge(`重试 ${maxRetry} 次后仍失败`);
return false;
}
六、总结:编写健壮脚本的黄金法则
- 防御性编程 :永远假设返回值可能是
null。 - 资源必回收 :
recycle()和releaseNode()必须放在finally中。 - 官方兜底 :使用
setExceptionCallback而非 Hack 全局对象。 - 双回调策略 :
setStopCallback+setExceptionCallback。 - 主动抛错 :使用
throw new Error()让逻辑更清晰。
如果你在脚本中遇到过匪夷所思的崩溃,欢迎把日志贴到评论区!