前言
大家好,我是鲫小鱼。是一名不写前端代码
的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!
第十八章:常见问题与解决方案
一、理论讲解:Auto.js 常见问题产生的根源与分类
1.1 为什么 Auto.js 脚本会遇到各种问题?
Auto.js 脚本运行在 Android 系统之上,其复杂性导致了各种问题的出现。理解这些问题的根源,有助于我们更好地定位和解决它们:
- Android 系统特性:Android 版本碎片化、OEM 厂商的定制、电池优化、权限管理、后台限制等。
- 应用自身变化:目标应用的 UI 更新、防自动化机制、版本差异等。
- 脚本开发问题:代码逻辑错误、性能瓶颈、资源管理不当、缺乏错误处理机制等。
- 设备差异:不同手机型号、屏幕分辨率、硬件性能等。
- 网络环境:网络不稳定、API 接口变化、数据格式不一致等。
1.2 常见问题分类
我们可以将 Auto.js 脚本开发中遇到的问题大致分为以下几类:
- 权限相关问题:无障碍服务未开启、存储权限、悬浮窗权限、Root 权限等。
- UI 自动化问题:控件找不到、点击无效、滑动失败、界面卡顿、弹窗阻碍等。
- 脚本运行问题:脚本崩溃、ANR (Application Not Responding)、后台被杀、定时任务不准时、多线程死锁等。
- 数据处理问题:文件读写失败、数据解析错误、网络请求失败、数据存储丢失等。
- 兼容性问题:不同 Android 版本、不同设备、不同目标应用版本导致的问题。
- 调试与日志问题:日志不全、无法定位错误、调试困难等。
- 反自动化问题:被目标应用检测并限制、验证码识别失败等。
二、代码示例:常见问题场景与实战解决方案
2.1 权限问题:请求必要权限
很多 Auto.js 功能需要特定权限,如无障碍服务、悬浮窗、存储权限等。脚本启动时应检查并引导用户授予。
javascript
// 场景:检查并请求无障碍服务、悬浮窗、存储权限
/**
* 检查并请求所有必要权限
*/
function requestNecessaryPermissions() {
log("检查并请求必要权限...");
// 1. 请求无障碍服务权限 (必须)
if (!auto.service) {
toast("请开启无障碍服务以便运行脚本");
app.startActivity({ packageName: "com.android.settings", className: "com.android.settings.Settings$AccessibilitySettingsActivity" });
// 或者使用更直接的方式,但可能需要用户手动确认
// app.startActivity("android.settings.ACCESSIBILITY_SETTINGS");
log("等待用户开启无障碍服务...");
// 可以添加一个循环等待,直到服务开启
while (!auto.service) {
sleep(1000);
}
log("无障碍服务已开启!");
}
// 2. 请求悬浮窗权限 (如果脚本需要悬浮窗)
if (!floaty.checkPermission()) {
toast("请授予悬浮窗权限");
floaty.requestPermission();
log("等待用户授予悬浮窗权限...");
while (!floaty.checkPermission()) {
sleep(1000);
}
log("悬浮窗权限已授予!");
}
// 3. 请求外部存储读写权限 (如果脚本需要读写文件,Android 6.0+ 需要动态申请)
// 在 Auto.js 中,files 模块通常会自动处理,但明确请求更保险
if (device.sdkInt >= 23 && !files.requestStoragePermission()) {
toast("请授予存储权限");
log("等待用户授予存储权限...");
while (!files.requestStoragePermission()) {
sleep(1000);
}
log("存储权限已授予!");
}
log("所有必要权限已检查并就绪!");
}
// 在脚本开始时调用
// requestNecessaryPermissions();
2.2 UI 自动化问题:控件找不到与动态 UI 适配
UI 自动化是最容易受应用版本影响的部分。需要更灵活的查找策略和错误处理。
javascript
// 场景:查找一个可能变化的按钮,并进行点击
/**
* 健壮的点击函数:尝试多种查找方式,并重试
* @param {Array<string>} texts 按钮可能的文本列表
* @param {string} id 按钮可能的ID
* @param {string} desc 按钮可能的描述
* @param {number} timeout 查找超时时间 (毫秒)
* @returns {boolean} 是否成功点击
*/
function robustClick(texts, id, desc, timeout = 3000) {
let startTime = Date.now();
while (Date.now() - startTime < timeout) {
let btn = null;
// 1. 尝试按ID查找
if (id) {
btn = $(id).findOne(200);
if (btn) {
log(`通过ID找到控件: ${id}`);
}
}
// 2. 如果未找到,尝试按文本列表查找
if (!btn && texts && texts.length > 0) {
for (let t of texts) {
btn = text(t).findOne(200);
if (btn) {
log(`通过文本找到控件: ${t}`);
break;
}
}
}
// 3. 如果未找到,尝试按描述查找
if (!btn && desc) {
btn = desc(desc).findOne(200);
if (btn) {
log(`通过描述找到控件: ${desc}`);
}
}
if (btn) {
try {
if (btn.clickable()) {
btn.click();
log("控件点击成功!");
return true;
} else if (btn.parent() && btn.parent().clickable()) { // 尝试点击父控件
btn.parent().click();
log("点击父控件成功!");
return true;
} else { // 无法点击,尝试坐标点击
click(btn.bounds().centerX(), btn.bounds().centerY());
log("通过坐标点击成功!");
return true;
}
} catch (e) {
log(`点击控件时发生异常: ${e}`, "ERROR");
}
}
sleep(500); // 每次尝试之间稍作等待
}
log("未能在指定时间内找到并点击控件。", "WARN");
return false;
}
// 示例使用
// robustClick(["确定", "OK"], "confirm_btn", "确认按钮");
// robustClick(["取消", "Cancel"], null, "取消按钮");
2.3 脚本崩溃/ANR 与后台被杀问题
- ANR (Application Not Responding):通常是主线程长时间阻塞导致。解决方案是将耗时操作移至子线程。
- 后台被杀:Android 系统为了省电,可能杀死后台进程。使用前台服务或定时任务可以提高脚本存活率。
javascript
// 场景:耗时任务在子线程执行,防止ANR;使用前台服务保持后台运行
function longRunningTask() {
log("开始执行耗时任务...");
sleep(5000); // 模拟耗时5秒
log("耗时任务完成。");
}
// 推荐:将耗时操作放到子线程
// threads.start(function(){
// longRunningTask();
// });
// log("主线程继续执行,耗时任务在后台进行...");
// 使用前台服务 (保持脚本后台运行,避免被系统杀死)
// 需要在 Auto.js Pro 的设置中开启"保持服务运行"或"前台服务"选项
// service.foreground = true; // 设置为前台服务
// log("脚本已设置为前台服务运行。");
// setInterval(() => {
// log("前台服务心跳...");
// }, 10000); // 每10秒打印一次心跳日志
// 更好的方式是使用 `engines.service` 模块
/*
// main.js (启动脚本)
if (!auto.service) {
toast("请先启动无障碍服务");
exit();
}
// 定义一个服务脚本 service.js
// engines.execScriptFile("service.js");
// service.js (服务脚本内容)
// setInterval(() => {
// log("服务正在运行...");
// // 在这里执行一些轻量级后台任务
// }, 5000);
// service.setOnDestroy(() => {
// log("服务被销毁");
// });
*/
2.4 数据处理问题:JSON 解析与文件操作
javascript
// 场景:安全地读写 JSON 配置文件
const CONFIG_FILE = "/sdcard/my_app_config.json";
/**
* 安全读取 JSON 配置文件
* @returns {object} 解析后的配置对象,或空对象
*/
function readJsonConfig() {
if (!files.exists(CONFIG_FILE)) {
log("配置文件不存在,创建默认配置。", "WARN");
return {};
}
try {
let content = files.read(CONFIG_FILE);
return JSON.parse(content);
} catch (e) {
log(`读取或解析配置文件失败: ${e}`, "ERROR");
return {};
}
}
/**
* 安全写入 JSON 配置文件
* @param {object} configData 要写入的配置数据
* @returns {boolean} 是否成功写入
*/
function writeJsonConfig(configData) {
try {
let content = JSON.stringify(configData, null, 4); // 格式化输出,方便阅读
files.write(CONFIG_FILE, content);
log("配置文件写入成功。");
return true;
} catch (e) {
log(`写入配置文件失败: ${e}`, "ERROR");
return false;
}
}
// 示例使用
// let config = readJsonConfig();
// config.username = "new_user";
// config.lastRun = Date.now();
// writeJsonConfig(config);
// log("当前配置:" + JSON.stringify(readJsonConfig()));
2.5 网络请求失败与错误重试
javascript
// 场景:带重试机制的网络请求
/**
* 带重试机制的 HTTP GET 请求
* @param {string} url 请求URL
* @param {number} retries 重试次数
* @param {number} delay 重试间隔 (毫秒)
* @returns {object|null} 响应对象,或 null
*/
function reliableHttpGet(url, retries = 3, delay = 2000) {
for (let i = 0; i < retries; i++) {
log(`尝试GET请求:${url} (第 ${i + 1} 次尝试)`);
try {
let res = http.get(url, { timeout: 5000 }); // 设置超时
if (res.statusCode == 200) {
log("请求成功!");
return res;
} else {
log(`请求失败,状态码:${res.statusCode}。` + (i < retries - 1 ? `等待 ${delay / 1000} 秒后重试...` : ""), "WARN");
}
} catch (e) {
log(`请求异常:${e}。` + (i < retries - 1 ? `等待 ${delay / 1000} 秒后重试...` : ""), "ERROR");
}
if (i < retries - 1) {
sleep(delay);
}
}
log("多次重试后请求仍失败。", "FATAL");
return null;
}
// 示例使用
// var response = reliableHttpGet("https://www.example.com/api/data", 5, 3000);
// if (response) {
// log("成功获取到数据:" + response.body.string().substring(0, 100) + "...");
// } else {
// log("未能获取数据。");
// }
2.6 反自动化检测与规避
很多应用会检测自动化行为。规避策略包括:
- 模拟真实用户行为:引入随机延时、随机滑动距离、随机点击位置偏差等。
- 使用图像识别:绕过控件 ID/文本的变化,直接根据图像特征判断。
- 降低操作频率:避免在短时间内进行大量重复操作。
- IP 代理:对于频繁的网络请求,更换 IP 地址。
- 模拟设备信息:修改设备型号、IMEI 等信息(Auto.js Pro 部分支持)。
javascript
// 场景:模拟更真实的用户点击
/**
* 模拟真实用户点击:带随机偏移和随机延时
* @param {UiObject} uiObject 目标控件
*/
function randomClick(uiObject) {
if (!uiObject) return;
let bounds = uiObject.bounds();
let x = bounds.centerX() + random(-5, 5); // x轴随机偏移
let y = bounds.centerY() + random(-5, 5); // y轴随机偏移
click(x, y);
sleep(random(500, 1500)); // 随机等待0.5到1.5秒
log(`模拟随机点击 (${x}, ${y})`);
}
// 示例使用
// let btn = text("立即领取").findOne();
// if (btn) {
// randomClick(btn);
// }
2.7 调试困难与日志技巧
- 善用
log()
/console.log()
:多在关键逻辑处打印日志,追踪变量值、函数执行流程。 - 日志分级:使用不同级别(INFO, WARN, ERROR, DEBUG)的日志。
- 文件日志:将日志输出到文件,方便长期存储和分析。
- Auto.js Pro 调试器:利用其断点、单步执行、变量查看等功能。
- Android Studio Logcat:配合 ADB 查看更底层的系统日志。
javascript
// 场景:详细日志记录与错误追踪
const DEBUG_MODE = true; // 控制是否输出DEBUG日志
const LOG_FILE_PATH = "/sdcard/script_debug.log";
function debugLog(msg) {
if (DEBUG_MODE) {
files.append(LOG_FILE_PATH, `[DEBUG] ${new Date().toLocaleString()} : ${msg}\n`);
console.log(`[DEBUG] ${msg}`);
}
}
function infoLog(msg) {
files.append(LOG_FILE_PATH, `[INFO] ${new Date().toLocaleString()} : ${msg}\n`);
log(`[INFO] ${msg}`);
}
function errorLog(msg, error) {
let errMsg = `[ERROR] ${new Date().toLocaleString()} : ${msg}`;+
if (error) {
errMsg += `\nStackTrace: ${error.stack}`; // 记录堆栈信息
}
files.append(LOG_FILE_PATH, errMsg + "\n");
log(errMsg, "e"); // 使用 'e' 级别在控制台显示红色
}
// 示例使用
// infoLog("脚本开始执行");
// let x = 10;
// debugLog("变量 x 的值为: " + x);
// try {
// throw new Error("模拟一个错误");
// } catch (e) {
// errorLog("捕获到错误!", e);
// }
// infoLog("脚本执行结束");
三、实战项目:脚本自愈与错误恢复模块
3.1 项目需求
开发一个能够自动处理常见异常并尝试恢复脚本运行的模块。当脚本在执行过程中遇到特定错误(如控件找不到、网络请求失败、意外弹窗)时,能自动进行错误识别、记录、尝试恢复,而不是直接崩溃退出。
3.2 项目结构
plaintext
autojs-self-healing-demo/
├── main.js # 主入口脚本,使用自愈模块
├── modules/
│ ├── errorRecovery.js # 核心错误恢复与处理逻辑
│ └── logger.js # 日志模块 (复用)
├── logs/
│ └── app.log
└── README.md
3.3 logger.js 日志模块 (复用,与前面章节相同)
javascript
// modules/logger.js
function log(msg, level = "INFO") {
let line = `[${new Date().toLocaleTimeString()}][${level}] ${msg}`;
files.append("../logs/app.log", line + "\n");
console.log(line); // 同时输出到控制台
}
module.exports = { log };
3.4 errorRecovery.js 错误恢复模块
javascript
// modules/errorRecovery.js
const logger = require('./logger.js');
const MAX_RETRY_COUNT = 3; // 最大重试次数
/**
* 尝试处理常见的运行时异常并进行恢复
* @param {Function} action 需要执行的业务逻辑函数
* @param {Object} options 配置项
* @param {Array<string>} [options.retryKeywords=[]] 遇到这些关键词的错误时进行重试
* @param {Array<string>} [options.popupKeywords=[]] 意外弹窗的关键词,自动关闭
* @param {Function} [options.onRecovery=null] 恢复操作前的回调函数
* @param {number} [options.maxRetries=MAX_RETRY_COUNT] 最大重试次数
* @param {number} [options.retryDelay=2000] 每次重试的间隔 (毫秒)
* @returns {boolean} 业务逻辑是否最终成功执行
*/
function executeWithRecovery(action, options = {}) {
const retryKeywords = options.retryKeywords || [];
const popupKeywords = options.popupKeywords || [];
const onRecovery = options.onRecovery || null;
const maxRetries = options.maxRetries || MAX_RETRY_COUNT;
const retryDelay = options.retryDelay || 2000;
let currentRetry = 0;
while (currentRetry <= maxRetries) {
try {
logger.log(`尝试执行业务逻辑 (第 ${currentRetry + 1} 次)...`);
action(); // 执行核心业务逻辑
logger.log("业务逻辑执行成功!");
return true;
} catch (e) {
logger.log(`业务逻辑执行失败,异常信息:${e.message}`, "ERROR");
logger.log(`堆栈信息:${e.stack}`, "ERROR");
// 1. 尝试处理意外弹窗
let popupHandled = false;
for (let keyword of popupKeywords) {
let uiObject = text(keyword).findOne(500) || desc(keyword).findOne(500);
if (uiObject) {
try {
uiObject.click();
logger.log(`自动关闭意外弹窗:'${keyword}'`, "INFO");
popupHandled = true;
sleep(1000); // 等待弹窗消失
break;
} catch (clickErr) {
logger.log(`点击弹窗失败 '${keyword}': ${clickErr}`, "ERROR");
}
}
}
if (popupHandled) {
logger.log("弹窗已处理,准备重试。");
sleep(retryDelay); // 弹窗处理后稍微等待
currentRetry++;
continue; // 处理完弹窗后立即重试当前操作
}
// 2. 检查是否需要重试 (基于错误信息关键词)
let shouldRetry = false;
for (let keyword of retryKeywords) {
if (e.message.includes(keyword)) {
shouldRetry = true;
break;
}
}
if (shouldRetry && currentRetry < maxRetries) {
logger.log(`检测到可重试错误,进行第 ${currentRetry + 1} 次重试...`, "WARN");
if (onRecovery) {
logger.log("执行恢复前回调...");
onRecovery(); // 执行自定义恢复操作,例如返回上一页、刷新界面
}
sleep(retryDelay); // 等待重试间隔
currentRetry++;
} else {
logger.log("达到最大重试次数或非可重试错误,脚本终止。", "FATAL");
return false; // 无法恢复,直接退出
}
}
}
logger.log("达到最大重试次数,业务逻辑未能成功执行。", "FATAL");
return false;
}
module.exports = { executeWithRecovery };
3.5 main.js 主入口脚本
javascript
// main.js
const errorRecovery = require('./modules/errorRecovery.js');
const logger = require('./modules/logger.js');
logger.log("脚本启动:开始执行带自愈功能的业务逻辑...");
let step = 0;
/**
* 模拟一个会抛出错误的业务逻辑
*/
function businessLogic() {
step++;
logger.log(`当前执行到业务逻辑第 ${step} 步。`);
if (step === 1) {
// 模拟一个控件找不到的错误
throw new Error("控件'下一步'未找到!");
} else if (step === 2) {
// 模拟一个意外弹窗 (实际环境中会通过 UI 查找识别)
logger.log("模拟出现一个意外弹窗:'系统提示:有新版本!'");
// 这里我们手动模拟一个UI弹窗,实际中可能是APP界面的弹窗
threads.start(function(){
"ui";
ui.layout(<frame><button id="updateBtn" text="立即更新" w="120dp" h="50dp"/><button id="cancelBtn" text="以后再说" w="120dp" h="50dp"/></frame>);
ui.cancelBtn.on("click",()=>{ ui.finish(); });
setTimeout(() => ui.finish(), 3000); // 3秒后自动消失
});
throw new Error("出现意外弹窗!"); // 抛出错误,让恢复模块处理
} else if (step === 3) {
// 模拟一个网络请求失败
throw new Error("网络请求失败:连接超时!");
} else if (step === 4) {
logger.log("最终业务逻辑成功执行!");
return true;
}
return false;
}
/**
* 自定义恢复操作:例如返回、刷新等
*/
function customRecoveryAction() {
logger.log("执行自定义恢复操作:返回上一页或刷新界面...");
back(); // 模拟返回上一页
sleep(1000);
// 也可以是刷新当前界面或重新初始化某些状态
}
// 执行业务逻辑并启用错误恢复机制
let success = errorRecovery.executeWithRecovery(businessLogic, {
retryKeywords: ["未找到", "失败", "超时"], // 遇到这些关键词的错误消息时重试
popupKeywords: ["以后再说", "取消", "关闭", "我知道了", "立即更新"], // 遇到这些关键词的弹窗时自动点击
onRecovery: customRecoveryAction, // 每次重试前的回调
maxRetries: 5, // 允许更多的重试次数
retryDelay: 1500 // 每次重试等待1.5秒
});
if (success) {
logger.log("恭喜!脚本成功克服困难完成任务!", "INFO");
} else {
logger.log("很遗憾,脚本在多次尝试后仍未能成功完成任务。", "FATAL");
}
logger.log("脚本执行完毕。");
exit();
3.6 运行效果与分析
- 运行
main.js
,脚本会模拟执行businessLogic()
函数。 - 在
businessLogic()
内部,我们模拟了控件找不到、意外弹窗和网络请求失败三种错误情况。 errorRecovery.js
模块会捕获这些异常,根据配置的retryKeywords
和popupKeywords
进行智能判断。- 当遇到"控件未找到"或"网络请求失败"时,会根据
retryKeywords
尝试重试,并调用customRecoveryAction
进行恢复。 - 当模拟的"意外弹窗"出现时,即使
businessLogic
抛出错误,errorRecovery
也会优先识别并关闭弹窗,然后继续重试。
- 当遇到"控件未找到"或"网络请求失败"时,会根据
- 日志会详细记录每次尝试、错误信息、恢复操作和最终结果。
- 这个项目展示了如何构建一个通用的自愈模块,提高脚本的容错性和健壮性,减少因小问题导致的脚本中断。
四、常见问题与解决方案总结
问题类型 | 常见具体问题 | 解决方案 |
---|---|---|
权限问题 | 无障碍服务未开启、悬浮窗权限未授权 | 脚本启动时主动 requestNecessaryPermissions() ,引导用户开启。 |
UI 自动化问题 | 控件找不到、点击无效、UI 变化失效 | id() 优先;使用 text() /desc() /className() 结合;局部查找;waitForExists() ;健壮点击函数 (robustClick );图像识别辅助。 |
脚本运行问题 | 脚本卡顿/ANR、后台被杀、定时任务不准时 | 耗时操作移至子线程;使用前台服务 (service.foreground = true );合理设置定时器 (setInterval );检查电池优化设置。 |
数据处理问题 | 文件读写失败、JSON 解析错误、数据丢失 | 使用 try-catch 捕获异常;检查文件路径和权限;安全地读写 JSON (readJsonConfig /writeJsonConfig );使用数据库。 |
网络请求问题 | 请求超时、请求失败、数据格式错误 | 设置超时时间;实现重试机制 (reliableHttpGet );检查网络连接;处理 HTTP 状态码;验证 JSON/XML 响应格式。 |
兼容性问题 | 不同 Android 版本、手机型号、应用版本适配 | 使用 device.sdkInt 进行版本判断;测试不同设备;使用更通用的 UI 查找方法;定期更新脚本。 |
反自动化检测 | 被目标应用识别、验证码、风控系统 | 模拟真实用户行为 (randomClick );引入随机延时;使用图像识别/OCR;IP 代理;降低操作频率。 |
调试与日志问题 | 难以定位错误、日志信息不足 | 善用 log() /console.log() ;日志分级 (debugLog /infoLog /errorLog );写入文件日志;利用 Auto.js Pro 调试器。 |
内存泄漏 | 脚本长时间运行导致内存占用过高 | 及时释放图片 (recycle() )、线程、定时器等资源;避免创建大量临时对象。 |
多线程安全 | 数据竞争、死锁 | 对共享资源使用 threads.lock() 或其他同步机制;避免在子线程直接操作 UI。 |
五、性能优化建议 (与问题解决关联)
性能优化和问题解决是相辅相成的。许多常见问题的解决方案本身就是性能优化的体现。
- UI 查找优化:精准定位控件,减少遍历,避免无效查找,是解决"控件找不到"和"脚本卡顿"的关键。
- 异步处理:将耗时操作放到子线程,是避免 ANR 和提升响应速度的有效手段。
- 资源管理:及时释放内存(特别是图片资源),是防止 OOM 的重要保障。
- 错误处理与重试:健壮的错误处理机制和重试策略,提高了脚本的容错性,减少因网络波动或UI变化导致的失败。
- 日志精简:控制日志输出,既能提高性能,又能让关键错误更易于发现。
- 模拟真实操作:这不仅是规避反自动化检测的手段,也通过引入适当延时,避免了因操作过快而导致的问题。
通过综合运用这些策略,我们可以编写出更加稳定、高效和健壮的 Auto.js 自动化脚本。
感谢大家阅读到这里,本系列的 Auto.js 学习已经结束,希望对大家有所帮助。
最后感谢阅读!欢迎关注我,微信公众号:
《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!!