EasyClick 入门指南(九):异常处理与脚本健壮性 —— 从“不堪一击”到“金刚不坏”

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;
}

六、总结:编写健壮脚本的黄金法则

  1. 防御性编程 :永远假设返回值可能是 null
  2. 资源必回收recycle()releaseNode()必须放在 finally中。
  3. 官方兜底 :使用 setExceptionCallback而非 Hack 全局对象。
  4. 双回调策略setStopCallback+ setExceptionCallback
  5. 主动抛错 :使用 throw new Error()让逻辑更清晰。

如果你在脚本中遇到过匪夷所思的崩溃,欢迎把日志贴到评论区!

相关推荐
大刚测试开发实战8 小时前
TestHub测试平台整体功能简介
django·llm·测试
ClouGence8 小时前
不用写 Selenium,零代码的 UI 自动化测试工具!
selenium·测试
大刚测试开发实战10 小时前
年度重磅!TestHub测试平台正式开源!
github·测试
tang&2 天前
【测试】Web页面UI自动化测试完全指南:8步通用测试框架
ui·测试
蓝核4 天前
basic_pentesting_2靶场实战[超详细教程]
测试
Jiude5 天前
AI面对真机调试也束手无策?我将方法论形成了一套SKILL 🛠️🤖
前端·后端·测试
胡图图不糊涂^_^5 天前
白盒测试——动态测试——逻辑覆盖法
笔记·测试·动态测试·白盒测试·逻辑覆盖法
胡图图不糊涂^_^6 天前
测试BUG篇
学习·bug·测试