前言
大家好,我是鲫小鱼。是一名不写前端代码
的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!
第十七章:性能优化建议
一、理论讲解:Auto.js 性能优化的重要性与常见瓶颈
1.1 为什么需要性能优化?
在 Auto.js 脚本开发中,性能优化不仅仅是为了让脚本运行得更快,更是为了:
- 提升用户体验:流畅的脚本运行能减少卡顿、等待,使用户感知更好。
- 节省设备资源:减少 CPU、内存、电量消耗,延长设备续航。
- 提高脚本稳定性:低效的脚本可能导致 ANR (Application Not Responding) 错误,或被系统后台杀死。
- 减少执行时间:对于需要快速响应或频繁执行的自动化任务至关重要。
- 应对复杂场景:在处理大量数据、复杂 UI 交互时,性能优化能确保脚本正常运行。
1.2 Auto.js 脚本的常见性能瓶颈
理解瓶颈所在是优化的第一步:
- UI 查找与操作 :
text()
,id()
,findOne()
,find()
等操作会遍历屏幕控件树,频繁或低效的查找是主要瓶颈。 - 循环与递归:大量循环或深度递归可能导致 CPU 占用过高,甚至栈溢出。
- 文件 I/O:频繁的读写文件,尤其是大文件,会造成性能下降。
- 内存管理:大量创建对象、未及时释放资源可能导致内存溢出 (OOM)。
- 网络请求:同步网络请求、网络超时、频繁短连接、大数据量传输。
- 图像识别与图色分析 :
images.findImage()
,colors.findColor()
等操作计算量大,是典型的 CPU 密集型任务。 - 多线程管理:线程创建销毁开销大,线程间通信不当可能导致死锁或资源竞争。
- 日志输出:在生产环境大量输出日志会增加 I/O 负担,影响性能。
- 不必要的等待 :
sleep()
时间过长或无谓的等待会拖慢脚本。
1.2.1 UI 自动化操作的深度优化
UI 自动化是 Auto.js 的核心,但也是最常见的性能瓶颈。优化不仅仅是减少查找次数,更要理解查找机制和替代方案。
- 查找策略的选择 :
id()
优先 :如果目标控件有唯一 ID,始终优先使用id()
,它的查找速度最快。text()
/desc()
:次选。它们需要遍历所有可见文本或描述进行匹配,效率低于id()
。className()
/bounds()
:作为补充。className()
查找可能返回大量结果,需要结合其他条件筛选;bounds()
适合做区域点击,但不适合查找复杂控件。
- 链式查找与缓存 :避免重复查找。如果需要对同一控件进行多次操作,应先
findOne()
并保存引用,后续直接使用。
javascript
// 不推荐:重复查找
// text("确定").click();
// text("确定").longClick();
// 推荐:缓存控件对象
let okButton = text("确定").findOne();
if (okButton) {
okButton.click();
sleep(500);
okButton.longClick();
}
- 局部查找 :当目标控件在某个特定父控件下时,利用
uiObject.find()
或uiObject.findOne()
在局部范围内查找,减少全局遍历。
javascript
// 假设要在一个卡片内部查找一个按钮
let card = id("card_container").findOne();
if (card) {
let buttonInCard = card.text("点击进入").findOne();
if (buttonInCard) {
buttonInCard.click();
}
}
waitFor
系列函数 :使用waitForExists()
,waitForActivities()
等函数代替简单的sleep()
,等待特定条件满足,避免死等或操作过早。
javascript
// 优化前:简单sleep,可能过长或过短
// sleep(3000); // 等待页面加载
// text("按钮").click();
// 优化后:等待按钮出现,提高效率和健壮性
if (text("按钮").waitForExists(5000)) {
text("按钮").click();
} else {
log("按钮未在5秒内出现");
}
- 控件层级优化:避免不必要的深层遍历,如果知道控件大致位置,可以先定位其父级或兄弟控件,再进行相对查找。
1.2.2 循环与算法效率
- 减少不必要的循环次数:在数据处理前进行筛选,避免在循环中执行重复或不必要的计算。
- 优化数据结构和算法:选择适合场景的数据结构(如 Map/Set 替代数组查找),优化算法复杂度 (O(n) -> O(log n))。
- 批量操作 :如果需要对多个相似控件进行操作,尽量一次性
find()
所有目标,再进行遍历,而不是每次循环都findOne()
。
javascript
// 不推荐:循环内重复查找和点击
// for (let i = 0; i < 5; i++) {
// let item = text("项目" + i).findOne();
// if (item) item.click();
// }
// 推荐:一次性查找所有,再遍历点击
let items = ("textStartsWith", "项目").find();
for (let i = 0; i < items.length; i++) {
items[i].click();
sleep(200); // 适当延时,模拟用户操作,避免过快
}
1.2.3 内存管理与资源释放
- 避免内存泄漏 :
- 对于
threads.start()
创建的子线程,在不需要时调用thread.interrupt()
中断并释放。 - 对于
setInterval
,setTimeout
创建的定时器,在不再需要时调用clearInterval
,clearTimeout
清除。 - 对于
ui
相关的监听器,在 Activity 销毁时解除注册。
- 对于
- 及时释放大对象 :当处理完图片、大量数据等大对象后,将其设置为
null
,以便垃圾回收器回收。
javascript
// 图片处理示例
let img = images.read("/sdcard/screenshot.png");
// ... 对 img 进行操作 ...
img.recycle(); // 显式释放图片占用的内存
img = null; // 帮助垃圾回收
- 数据压缩:对于需要存储或传输的大量数据,考虑进行压缩(如 JSON.stringify 后再进行 Base64 编码或 Gzip 压缩),减少内存占用和 I/O。
1.2.4 文件 I/O 优化
- 减少读写频率:避免在循环中频繁读写文件。可以先将数据累计到内存中,再批量写入。
- 选择合适的存储方式 :
files
模块:适合读写文本、JSON、日志等文件。storage
模块:适合轻量级键值对存储,内部已优化。- 数据库 (SQLite):适合结构化、大量数据存储和复杂查询,比文件读写更高效。
- 缓存文件内容:对于经常读取的配置文件、数据文件,可以读取一次后缓存到内存中,减少磁盘 I/O。
javascript
// 不推荐:频繁读取配置文件
// function getConfig(key) {
// let config = JSON.parse(files.read("/sdcard/config.json"));
// return config[key];
// }
// log(getConfig("user"));
// log(getConfig("password"));
// 推荐:一次性加载到内存并缓存
let cachedConfig = null;
function getCachedConfig() {
if (!cachedConfig) {
try {
cachedConfig = JSON.parse(files.read("/sdcard/config.json"));
} catch (e) {
log("读取配置文件失败: " + e, "ERROR");
cachedConfig = {};
}
}
return cachedConfig;
}
log(getCachedConfig().user);
log(getCachedConfig().password);
1.2.5 网络请求优化
- 异步请求 :总是使用
http.get()
,http.post()
等的异步版本(带回调函数),避免阻塞主线程。 - 请求合并:将多个小请求合并成一个大请求,减少网络开销。
- 数据压缩:请求和响应数据量大时,可以协商使用 Gzip 等压缩。
- 错误重试与超时:合理设置超时时间,对网络错误进行重试,但要注意重试策略,避免无限重试。
- 缓存网络数据:对于不经常变化的数据,可以缓存到本地,减少重复请求。
1.2.6 多线程与异步操作优化
- 合理使用线程池:避免频繁创建和销毁线程,对于需要执行大量并发任务的场景,使用线程池可以复用线程,减少开销。
- 线程安全 :对共享数据进行加锁保护 (
threads.lock()
), 避免数据竞争和不一致。 - 避免 UI 阻塞 :所有耗时操作都应在子线程中执行,UI 更新必须通过
ui.run()
调度到主线程。 - 线程间通信 :使用
events
模块或Handler
进行线程间通信,避免轮询或不当的等待。
1.2.7 日志输出优化
- 日志分级:在开发阶段可以打印详细日志,但在生产环境中应限制日志等级,只输出必要的 ERROR 或 WARN 级别日志,减少 I/O。
- 异步日志写入:如果日志量非常大,可以将日志写入操作放到一个单独的子线程中,异步进行,避免阻塞主线程。
- 定期清理日志:防止日志文件过大占用存储空间。
1.2.8 其他通用优化技巧
- 代码精简与优化:删除不必要的代码、变量、函数。优化算法逻辑。
- 预加载:对于启动时需要加载的模块或资源,可以考虑预加载,减少运行时延时。
- 利用 Pro 版特性:Auto.js Pro 提供了更强大的混淆、打包、加密等功能,可以进一步优化脚本性能和安全性。
- Profile 工具:利用 Auto.js Pro 的性能分析工具,定位脚本的 CPU、内存热点,进行针对性优化。
二、代码示例:Auto.js 性能优化全场景实战
2.1 UI 查找效率对比与优化
javascript
// 场景:查找一个特定的文本控件并点击
// 未优化:每次都进行全局查找
function inefficientClick() {
let startTime = Date.now();
for (let i = 0; i < 5; i++) {
let target = text("目标文本").findOne(1000);
if (target) {
target.click();
log(`低效点击:第${i+1}次`);
}
}
log(`未优化耗时:${Date.now() - startTime}ms`);
}
// 优化一:缓存控件对象
function optimizedClickCache() {
let startTime = Date.now();
let target = text("目标文本").findOne(1000); // 第一次查找并缓存
if (target) {
for (let i = 0; i < 5; i++) {
target.click();
log(`缓存优化点击:第${i+1}次`);
}
} else {
log("目标文本未找到");
}
log(`缓存优化耗时:${Date.now() - startTime}ms`);
}
// 优化二:局部查找 (假设目标文本在一个 ID 为 'container' 的控件内)
function optimizedClickLocal() {
let startTime = Date.now();
let container = id("container").findOne(1000);
if (container) {
for (let i = 0; i < 5; i++) {
let target = container.text("目标文本").findOne(500); // 在局部内查找
if (target) {
target.click();
log(`局部查找优化点击:第${i+1}次`);
}
}
} else {
log("容器未找到");
}
log(`局部查找优化耗时:${Date.now() - startTime}ms`);
}
// 实际运行时需要有相应的UI界面供测试
// console.show();
// toastLog("请确保屏幕上有包含'目标文本'的控件,以及ID为'container'的容器");
// sleep(3000);
// inefficientClick();
// sleep(1000);
// optimizedClickCache();
// sleep(1000);
// optimizedClickLocal();
2.2 大数据量数组处理效率对比
javascript
// 场景:从一个大型数组中查找特定元素
let largeArray = [];
for (let i = 0; i < 10000; i++) {
largeArray.push({ id: i, name: "Item " + i });
}
// 未优化:线性查找
function inefficientSearch(arr, id) {
let startTime = Date.now();
for (let i = 0; i < arr.length; i++) {
if (arr[i].id === id) {
log(`低效查找耗时:${Date.now() - startTime}ms`);
return arr[i];
}
}
log(`低效查找耗时:${Date.now() - startTime}ms`);
return null;
}
// 优化:使用 Map 结构进行快速查找
let dataMap = new Map();
for (let item of largeArray) {
dataMap.set(item.id, item);
}
function optimizedSearch(map, id) {
let startTime = Date.now();
let result = map.get(id);
log(`Map优化查找耗时:${Date.now() - startTime}ms`);
return result;
}
// console.show();
// inefficientSearch(largeArray, 9999);
// optimizedSearch(dataMap, 9999);
2.3 内存优化:图片资源释放
javascript
// 场景:处理多张图片,避免内存溢出
function processImagesEfficiently(imagePaths) {
for (let i = 0; i < imagePaths.length; i++) {
let path = imagePaths[i];
log(`正在处理图片:${path}`);
let img = images.read(path);
if (img) {
// 模拟对图片进行复杂操作,例如截取、识别
// let croppedImg = images.clip(img, 0, 0, 100, 100);
// log(images.detectsText(croppedImg));
img.recycle(); // 显式释放图片占用的内存
img = null; // 帮助垃圾回收器回收
log(`图片 ${path} 处理完成并释放内存。`);
} else {
log(`无法读取图片:${path}`, "ERROR");
}
sleep(500); // 模拟处理间隔
}
log("所有图片处理完毕。");
}
// 假设有这些图片文件 (实际运行需要手动创建或提供路径)
// var paths = [
// "/sdcard/Pictures/img1.png",
// "/sdcard/Pictures/img2.png",
// "/sdcard/Pictures/img3.png"
// ];
// processImagesEfficiently(paths);
2.4 文件 I/O 优化:批量写入日志
javascript
// 场景:频繁记录日志,避免频繁写入文件
let logBuffer = [];
let logTimer = null;
const LOG_FILE = "/sdcard/app_log_optimized.txt";
const FLUSH_INTERVAL = 2000; // 每2秒写入一次文件
/**
* 优化后的日志函数:将日志缓存起来,定时批量写入
* @param {string} msg 日志消息
*/
function bufferedLog(msg) {
let line = `[${new Date().toLocaleTimeString()}] ${msg}`;
logBuffer.push(line);
console.log(line); // 实时输出到控制台
if (!logTimer) {
logTimer = setTimeout(() => {
flushLogs();
logTimer = null;
}, FLUSH_INTERVAL);
}
}
/**
* 强制将缓存中的日志写入文件
*/
function flushLogs() {
if (logBuffer.length > 0) {
let contentToFlush = logBuffer.join("\n") + "\n";
files.append(LOG_FILE, contentToFlush);
logBuffer = []; // 清空缓存
log("日志已批量写入文件。", "DEBUG");
}
}
// 确保脚本退出时所有日志都被写入
events.on("exit", function() {
flushLogs();
log("脚本退出,所有缓存日志已写入。");
});
// 模拟高频日志生成
// for (let i = 0; i < 20; i++) {
// bufferedLog(`这是一个测试日志消息 ${i + 1}`);
// sleep(100); // 模拟短间隔操作
// }
// sleep(FLUSH_INTERVAL + 1000); // 等待最后一次刷新
2.5 多线程优化:异步网络请求与 UI 更新
javascript
// 场景:在子线程中进行网络请求,并在主线程更新 UI
"ui";
ui.layout(
<vertical padding="16">
<text id="statusText" textSize="16sp" text="点击按钮获取数据"/>
<button id="fetchBtn" text="获取网络数据"/>
</vertical>
);
ui.fetchBtn.on("click", () => {
ui.statusText.setText("正在获取数据...");
ui.fetchBtn.setEnabled(false); // 禁用按钮防止重复点击
threads.start(function() {
let res = null;
try {
res = http.get("https://www.baidu.com"); // 模拟耗时网络请求
} catch (e) {
log("网络请求失败: " + e, "ERROR");
}
ui.run(function() {
if (res && res.statusCode == 200) {
ui.statusText.setText(`数据获取成功!状态码: ${res.statusCode},内容长度: ${res.body.bytes().length}`);
} else {
ui.statusText.setText("数据获取失败或网络异常。");
}
ui.fetchBtn.setEnabled(true); // 重新启用按钮
});
});
});
// 启动脚本后,点击"获取网络数据"按钮观察效果。
// 日志中会显示网络请求的成功或失败信息,UI 界面会实时更新状态。
三、实战项目:优化一个效率低下的图片处理脚本
3.1 项目需求
一个自动化脚本需要频繁截屏并在截屏图片上进行颜色查找。目前的实现方式效率低下,导致脚本运行缓慢且容易崩溃。我们需要对其进行性能优化。
3.2 原始低效脚本 (inefficient_image_processor.js
)
javascript
// inefficient_image_processor.js
// 假设我们需要查找屏幕上是否有红色像素,并记录位置
const TARGET_COLOR = colors.rgb(255, 0, 0); // 纯红色
const CHECK_INTERVAL = 1000; // 每秒检查一次
const DURATION = 10 * 1000; // 持续运行10秒
log("启动低效图片处理脚本...");
let startTime = Date.now();
while (Date.now() - startTime < DURATION) {
let screenshot = images.captureScreen(); // 频繁截屏
if (screenshot) {
let foundPoint = colors.findColor(screenshot, TARGET_COLOR, {threshold: 10, region: [0, 0, device.width, device.height]}); // 全屏查找颜色
if (foundPoint) {
log(`在 (${foundPoint.x}, ${foundPoint.y}) 找到红色像素!`);
} else {
log("未找到红色像素。");
}
screenshot.recycle(); // 释放内存
} else {
log("截屏失败!", "ERROR");
}
sleep(CHECK_INTERVAL);
}
log(`低效图片处理脚本运行结束,总耗时:${Date.now() - startTime}ms`);
exit();
低效原因分析:
- 频繁截屏 :
images.captureScreen()
是一个耗时操作,每秒执行一次会造成严重的性能负担。 - 全屏颜色查找 :
colors.findColor()
对整个屏幕进行像素遍历,计算量巨大。 - 缺乏目标区域限制:即使目标颜色只出现在屏幕的某个小区域,脚本依然会扫描整个屏幕。
3.3 优化后的脚本 (optimized_image_processor.js
)
javascript
// optimized_image_processor.js
// 优化后的图片处理脚本
const TARGET_COLOR = colors.rgb(255, 0, 0); // 纯红色
const CHECK_INTERVAL_OPTIMIZED = 2000; // 降低检查频率,每2秒检查一次
const DURATION_OPTIMIZED = 10 * 1000; // 持续运行10秒
// 假设我们知道红色像素通常出现在屏幕右下角的某个固定区域
const SEARCH_REGION = [device.width * 0.7, device.height * 0.7, device.width * 0.3, device.height * 0.3]; // 屏幕右下角30%区域
log("启动优化图片处理脚本...");
let startTimeOptimized = Date.now();
while (Date.now() - startTimeOptimized < DURATION_OPTIMIZED) {
let screenshot = null;
try {
// 优化1:减少截屏频率,并确保每次截屏成功
screenshot = images.captureScreen();
if (!screenshot) {
log("截屏失败,尝试重试...", "WARN");
sleep(500); // 稍等再重试
screenshot = images.captureScreen();
if (!screenshot) {
log("二次截屏失败,跳过本次检测。", "ERROR");
sleep(CHECK_INTERVAL_OPTIMIZED);
continue;
}
}
// 优化2:限制颜色查找区域
let foundPoint = colors.findColor(screenshot, TARGET_COLOR, {
threshold: 10,
region: SEARCH_REGION // 关键优化:只在指定区域查找
});
if (foundPoint) {
log(`在 (${foundPoint.x}, ${foundPoint.y}) 找到红色像素!`);
} else {
log("未找到红色像素。");
}
} catch (e) {
log(`图片处理异常: ${e}`, "ERROR");
} finally {
if (screenshot) {
screenshot.recycle(); // 确保每次截屏后释放内存
screenshot = null; // 帮助垃圾回收
}
}
sleep(CHECK_INTERVAL_OPTIMIZED);
}
log(`优化图片处理脚本运行结束,总耗时:${Date.now() - startTimeOptimized}ms`);
exit();
3.4 运行效果与分析
- 性能提升:优化后的脚本通过减少截屏频率和限制颜色查找区域,显著降低了 CPU 占用和内存消耗,运行效率更高,响应更及时。
- 资源管理 :强制
recycle()
图片对象并将其设为null
,确保内存及时回收,避免了内存溢出。 - 健壮性增强:增加了截屏失败重试机制和异常捕获,提高了脚本的稳定性。
- 适用性 :通过
SEARCH_REGION
的配置,脚本更适用于目标颜色出现在特定区域的场景。
四、常见问题与解决方案
问题 | 解决方案 |
---|---|
脚本卡顿/ANR | 优化 UI 查找、减少循环次数、将耗时操作移至子线程、避免频繁文件 I/O。 |
内存溢出 (OOM) | 及时释放图片等大对象 (recycle() );避免创建大量临时对象;优化数据结构。 |
脚本运行缓慢 | 定位性能瓶颈 (CPU 密集型或 I/O 密集型);优化算法;减少不必要的等待。 |
子线程阻塞/死锁 | 确保子线程不执行 UI 操作;对共享资源加锁保护;合理设计线程间通信。 |
UI 自动化失败率高 | 增加等待时间、多条件查找、模拟真实操作路径、图片识别辅助。 |
日志过多影响性能 | 生产环境降低日志等级;使用异步日志写入;定期清理日志文件。 |
网络请求超时/失败 | 增加超时时间、实现重试机制、优化网络环境、使用 CDN。 |
电量消耗过快 | 降低脚本运行频率;减少后台活动;优化算法降低 CPU 占用;减少屏幕亮屏时间。 |
Auto.js Pro 特性无法利用 | 了解并充分利用 Pro 版提供的混淆、打包、性能分析工具、Java 扩展等高级功能。 |
复杂 UI 交互效率低 | 优先使用 id() 或 text() ;利用控件层级关系进行局部查找;考虑直接调用 Android API。 |
五、性能优化建议
- 性能分析先行:在优化之前,务必使用 Auto.js Pro 的性能分析工具 (如 CPU Profile, Memory Monitor) 定位真正的性能瓶颈,避免盲目优化。
- 资源利用最大化:充分利用设备的多核 CPU 能力,将独立的耗时任务分配到子线程中并行执行。
- 精益求精 :每次操作都问自己:"有没有更高效的方式?"例如,一个
click()
背后可能是复杂的控件查找,如果可以换成坐标点击
,则可能更快(但兼容性差)。 - 防范内存泄漏:养成良好的编程习惯,对于不再使用的对象(尤其是图片、大数组、线程、定时器)及时释放其资源和引用。
- 网络与文件 I/O 优化:减少网络请求次数,合并请求;文件读写采用批量操作和缓存机制,减少磁盘唤醒。
- 降低 UI 查找成本 :优先使用
id
或text
,利用局部查找,缓存已找到的控件,减少不必要的屏幕遍历。 - 算法与数据结构优化:针对数据处理密集型任务,选择合适的算法和数据结构(如哈希表、平衡树),提高查找、排序、计算效率。
- 日志管理:在生产环境中,限制日志输出的详细程度和频率,避免日志文件过大和频繁 I/O。
- 外部工具辅助:结合 ADB、Android Studio 的 Profiler 等工具进行更深层次的性能分析。
- 代码审查与重构:定期审查和重构代码,消除冗余、提高可读性和可维护性,这本身就是一种性能优化。
- 模拟真实用户行为:在自动化操作中引入少量随机延时,既能降低被反自动化检测的风险,又能避免过快的操作导致系统或应用响应不及。
最后感谢阅读!欢迎关注我,微信公众号:
《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!!