// ========== 核心颜色判断函数(扩展黑色系排除逻辑) ==========
// 判断是否为目标灰色1(#C3CCDB 色系,误差20)
function isGray1(color) {
var r = (color >> 16) & 0xFF;
var g = (color >> 8) & 0xFF;
var b = color & 0xFF;
var baseR = 195, baseG = 204, baseB = 219;
var diffR = Math.abs(r - baseR);
var diffG = Math.abs(g - baseG);
var diffB = Math.abs(b - baseB);
return diffR < 20 && diffG < 20 && diffB < 20;
}
// 判断是否为目标灰色2(#DFE0E4 色系,误差20)
function isGray2(color) {
var r = (color >> 16) & 0xFF;
var g = (color >> 8) & 0xFF;
var b = color & 0xFF;
var baseR = 223, baseG = 224, baseB = 228;
var diffR = Math.abs(r - baseR);
var diffG = Math.abs(g - baseG);
var diffB = Math.abs(b - baseB);
return diffR < 20 && diffG < 20 && diffB < 20;
}
// 判断是否为 #CDCDCD 色系(误差20)
function isGray3(color) {
var r = (color >> 16) & 0xFF;
var g = (color >> 8) & 0xFF;
var b = color & 0xFF;
var baseR = 205, baseG = 205, baseB = 205;
var diffR = Math.abs(r - baseR);
var diffG = Math.abs(g - baseG);
var diffB = Math.abs(b - baseB);
return diffR < 20 && diffG < 20 && diffB < 20;
}
// 核心修改:扩展黑色系判断,包含#282A29/#292A2C/#7B7D7C及相近色
function isBlackSeries(color) {
var r = (color >> 16) & 0xFF;
var g = (color >> 8) & 0xFF;
var b = color & 0xFF;
// 定义需要排除的黑色系基准色(可继续新增)
var blackTargets = [
{r: 40, g: 42, b: 41}, // #282A29
{r: 41, g: 42, b: 44}, // #292A2C
{r: 123, g: 125, b: 124} // #7B7D7C
];
// 遍历所有基准色,只要与任一基准色差值<30(放宽误差,覆盖相近色),就判定为黑色系
for (var i = 0; i < blackTargets.length; i++) {
var target = blackTargets[i];
var diffR = Math.abs(r - target.r);
var diffG = Math.abs(g - target.g);
var diffB = Math.abs(b - target.b);
if (diffR < 100 && diffG < 100 && diffB < 100) {
return true;
}
}
return false;
}
// 判断是否为白色(#FDFDFD/#FFFFFF 色系,误差20)
function isWhite(color) {
var r = (color >> 16) & 0xFF;
var g = (color >> 8) & 0xFF;
var b = color & 0xFF;
var target1R = 253, target1G = 253, target1B = 253;
var target2R = 255, target2G = 255, target2B = 255;
var diff1R = Math.abs(r - target1R);
var diff1G = Math.abs(g - target1G);
var diff1B = Math.abs(b - target1B);
var diff2R = Math.abs(r - target2R);
var diff2G = Math.abs(g - target2G);
var diff2B = Math.abs(b - target2B);
return (diff1R < 20 && diff1G < 20 && diff1B < 20) ||
(diff2R < 20 && diff2G < 20 && diff2B < 20);
}
// 整合判断:是否为需要避开的颜色(替换为新的黑色系判断)
function isAvoidColor(color) {
return isGray1(color) || isGray2(color) || isGray3(color) || isBlackSeries(color) || isWhite(color);
}
// ========== 主执行函数(无修改,保留容错逻辑) ==========
function main() {
try {
// 1. 等待无障碍服务就绪
//auto.waitFor();
sleep(800);
// 2. 申请截图权限
if (!requestScreenCapture()) {
toastLog('未授予 Hamibot 屏幕截图权限,请手动授权');
return;
}
sleep(1000);
// 3. 捕获屏幕(空值判断容错)
var img = captureScreen();
if (!img) {
toastLog('截图失败,无法获取屏幕画面');
return;
}
// 4. 基础参数计算
var width = img.getWidth();
var height = img.getHeight();
var centerX = Math.round(width / 2);
var centerY = Math.round(height / 2);
var sampleStep = 20;
var points = [];
// 5. 采样点收集
for (var y = 0; y < height; y += sampleStep) {
for (var x = 0; x < width; x += sampleStep) {
var distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));
points.push({x: x, y: y, distance: distance});
}
}
// 6. 按距离排序
points.sort(function(a, b) {
return a.distance - b.distance;
});
var foundPoint = null;
// 7. 遍历找有效点(坐标范围容错)
for (var i = 0; i < points.length; i++) {
var point = points[i];
if (point.x < 0 || point.x >= width || point.y < 0 || point.y >= height) {
continue;
}
var color = images.pixel(img, point.x, point.y);
if (!isAvoidColor(color)) { // 自动排除所有黑色系+灰色+白色
foundPoint = point;
break;
}
}
// 8. 执行点击
if (foundPoint) {
toastLog('找到目标区域,点击位置: ' + foundPoint.x + ', ' + foundPoint.y);
Click(foundPoint.x, foundPoint.y)
sleep(500);
} else {
toastLog('没有找到非灰色/非白色/非黑色系区域');
}
// 9. 释放图片资源
img.recycle();
//img = null;
} catch (e) {
toastLog('脚本执行异常:' + e.message + ',行号:' + (e.lineNumber || '未知'));
if (img && img.recycle) img.recycle();
} finally {
sleep(300);
hamibot.exit();
}
}
// 执行主函数
main();
这个为什么运行第二次hamibot就会奔溃
if (!requestScreenCapture()) {
toastLog('没有授予 Hamibot 屏幕截图权限');
hamibot.exit();
}
sleep(1000);
img = captureScreen();
sleep(800);
res = ocr.recognize(img);
为什么每次使用截图功能,脚本完成后,hamibot就会退出,离线?必须重新开启hamibot
if (!requestScreenCapture()) {
toastLog('没有授予 Hamibot 屏幕截图权限');
hamibot.exit();
}
我指的是,只要执行完这个,hamibot就会自动奔溃,下次再使用hamibot,就要重新启动hamibot app,而没有这个是不会出现的。
回答
你的脚本在第二次运行时崩溃,主要原因是图片资源管理问题。Hamibot 的截图资源需要严格遵循"获取-使用-释放"的生命周期管理,而你的代码存在两个关键问题:
问题分析
-
双重释放风险:
// try 块中的释放 img.recycle(); // catch 块中的条件释放 if (img && img.recycle) img.recycle();当 try 块中已成功执行
img.recycle()后,img 对象可能已处于无效状态。此时 catch 块中的条件检查img.recycle可能仍为 true(Rhino 引擎特性),导致对已释放对象再次调用 recycle(),引发 JNI 层崩溃。 -
资源残留问题:
Hamibot 的截图资源基于 Android Bitmap 实现,未正确释放会导致:- 第一次运行:占用 ~5-15MB 内存(取决于屏幕分辨率)
- 第二次运行:系统无法分配新的 Bitmap 内存,触发 OOM 崩溃
解决方案
修改 finally 块为单次确定性释放:
} finally {
// 文档地址 https://docs.hamibot.com/
if (img) {
try {
img.recycle(); // 确保只释放一次
} catch (e) {}
img = null; // 切断引用
}
sleep(300);
hamibot.exit();
}
其他改进建议
-
修正点击函数:
// 错误:Click (大写C) Click(foundPoint.x, foundPoint.y) // 正确:click (小写c) click(foundPoint.x, foundPoint.y) -
优化黑色系判断(避免误判):
// 原代码阈值100过大(0-255范围),实际应≤30 if (diffR < 30 && diffG < 30 && diffB < 30) { return true; } -
添加资源获取重试机制:
var img; for (var i = 0; i < 3; i++) { img = captureScreen(); if (img) break; sleep(500); } if (!img) { toastLog("截图失败"); return; }
为什么第一次能运行?
- 第一次运行时系统有足够内存分配新 Bitmap
- 第二次运行时因前次资源未完全释放,系统拒绝分配新内存
Hamibot 对截图资源管理非常严格,必须保证:
- 每次
captureScreen()对应且仅对应一次recycle()- 在 finally 块中执行释放操作
- 释放后立即将引用置为 null
按照上述修改后,脚本可以稳定重复运行。建议在关键资源操作处添加日志:
toastLog("开始释放截图资源");
img.recycle();
toastLog("资源释放完成");
补充回答 1
这是正常行为,不是bug。Hamibot设计就是脚本执行完毕后自动停止运行,这是预期的工作方式。
在你提供的代码中:
if (!requestScreenCapture()) {
toastLog('没有授予 Hamibot 屏幕截图权限');
hamibot.exit();
}
sleep(1000);
img = captureScreen();
sleep(800);
res = ocr.recognize(img);
这段代码执行完最后一行ocr.recognize(img)后,脚本已经完成所有指令,自然就会退出。这和你看到的"Hamibot退出,离线"现象完全符合设计逻辑。
如果你希望:
- 脚本执行完有提示再退出:在代码最后添加
toastLog("任务完成"); sleep(2000); - 脚本持续运行不退出:用循环结构包裹代码
while(true) { // 你的截图和识别代码 sleep(5000); // 每5秒执行一次 } - 执行一次后等待用户操作:添加等待指令
toastLog("任务完成,点击通知栏停止按钮可手动结束"); events.on("exit", function() { toastLog("脚本已手动停止"); }); // 保持脚本运行 while(true) { sleep(1000); }
参考文档中的示例代码通常都包含hamibot.exit(),就是为了明确表示脚本结束点。如果你看知识库中的示例,结尾都有hamibot.exit(),执行到这里就会正常退出。
这是Hamibot的标准工作流程:启动脚本 → 执行代码 → 代码结束 → 自动停止脚本,不是程序崩溃或异常退出。
补充回答 2
这确实是异常行为,不是正常现象。正常情况下,requestScreenCapture()只会申请截图权限,脚本执行完毕后Hamibot应该只是停止脚本运行,但应用本身应该保持在后台运行状态,不需要重新启动应用。
这个问题可能有以下原因及解决方案:
可能原因:
- 截图权限未正确释放 - 在某些Android设备上,截图权限如果没有正确释放会导致系统服务异常
- 特定设备兼容性问题 - 尤其是小米、华为等国产定制ROM对截图权限有特殊处理
- 脚本中缺少必要的资源释放代码
解决方案:
- 在脚本结束前添加截图服务释放代码:
if (!requestScreenCapture()) {
toastLog('没有授予 Hamibot 屏幕截图权限');
hamibot.exit();
}
sleep(1000);
// 你的截图和识别代码
img = captureScreen();
sleep(800);
res = ocr.recognize(img);
// 重要:在退出前释放截图服务
if (typeof releaseScreenCapture === 'function') {
releaseScreenCapture();
}
toastLog("任务完成,2秒后退出");
sleep(2000);
hamibot.exit();
-
确保在Hamibot界面内执行截图请求(这点非常重要):
- 根据文档说明:"建议在本软件界面运行该函数,在其他软件界面运行时容易出现一闪而过的黑屏现象"
- 应该先确保你处于Hamibot应用的主界面,再启动脚本
-
更新Hamibot到最新版本:
- 检查是否有新版本,旧版本可能存在已知的截图服务bug
- 前往Hamibot官网或应用商店下载最新版本
-
对于特定品牌手机的特殊设置:
- 小米手机:设置 → 应用设置 → 权限管理 → Hamibot → 允许后台弹出界面
- 华为手机:电池优化中将Hamibot设为"不优化"
- OPPO/vivo:权限管理中开启"后台高耗电"和"后台弹出界面"权限
如果以上方法仍不能解决问题,建议在Hamibot社区论坛提交详细问题报告,包括你的手机型号、Android版本和Hamibot版本,这可能是特定设备的兼容性问题,开发团队可能需要针对性修复。