写了一个悬浮窗代码,但是点击开始按钮以后,每次运行到gesture()以后,软件都会卡死,无响应,只能强制退出。在主线程里调用gesture则一点问题都没有。为什么会这样?悬浮窗回调函数对触摸操作有限制吗?以下为我的代码:
auto.waitFor();
var running = false;
var mission = null;
function OnlyGesture(){
gesture(500, [576, 1360], [555, 865]);
setTimeout(() => {
doTask()
}, 1000);
}
// 循环执行的任务
function doTask() {
if (!running) return;
setTimeout(() => {
toastLog("111");
}, 1000);
let delay = Math.ceil(Math.random() * 5) * 1000;
toastLog("下一次间隔为:" + delay.toString());
mission = setTimeout(() => {
doTask();
}, delay);
}
// 悬浮窗界面
let w = floaty.window(
<vertical padding="8" bg="#bababab4">
<text id="running">停止中</text>
<button id="start" text='开始' />
<button id="btn1" text='停止' />
<button id="btn3" text='退出' />
</vertical>
);
// 开始按钮的回调
w.start.click(() => {
if (running) {
toastLog("任务已经在执行!");
return;
}
running = true;
w.running.setText("运行中");
toastLog("任务启动成功");
setTimeout(OnlyGesture, 2000);
});
// 停止按钮
w.btn1.click(() => {
if (!running) {
toastLog("没有任务在运行!");
return;
}
running = false;
if (mission) {
clearTimeout(mission);
mission = null;
}
w.running.setText("停止中");
toastLog("任务已停止");
});
// 退出按钮
w.btn3.click(() => {
running = false;
if (mission) clearTimeout(mission);
w.close();
engines.myEngine().forceStop(); // 彻底退出脚本
});
// 悬浮窗基础设置
w.setAdjustEnabled(true);
w.setPosition(100, 100);
w.setSize(-2, -2);
// 脚本保活(必须加)
setInterval(() => { }, 60000 * 60);
//Math.ceil(Math.random()*5+10) 10到15随机整数
回答
这个问题出现的原因是线程阻塞。
原因分析
- UI 线程阻塞:
floaty.window的按钮点击回调(如w.start.click)运行在 UI 线程 上。UI 线程必须保持流畅以响应用户操作和绘制界面。 - 阻塞操作:
gesture()是一个阻塞式的自动化操作,执行时需要等待手势完成。 - 冲突:当你在 UI 线程(或通过 UI 线程触发的回调)中直接调用
gesture(),UI 线程会被阻塞等待手势完成,导致界面无法刷新,出现“卡死”或“无响应”(ANR)。虽然你使用了setTimeout,但在某些环境下,回调仍然可能关联到 UI 线程上下文,或者gesture执行时锁住了辅助服务通道,导致 UI 线程无法继续调度。
解决方案
- 使用子线程:将自动化任务(
gesture、循环逻辑)放入threads.start()创建的子线程中运行,避免占用 UI 线程。 - 安全更新 UI:在子线程中修改悬浮窗内容(如
setText)时,必须使用ui.run()包裹,因为 UI 控件只能在 UI 线程修改。 - 语法兼容:Hamibot 使用的 JavaScript 引擎(Rhino 1.7.15)不完全支持 ES6 箭头函数
()=>,建议全部改为function()以确保稳定性。
修正后的代码
以下是修复了线程问题并兼容引擎语法的代码:
// 文档地址 https://docs.hamibot.com/
auto.waitFor();
var running = false;
var taskThread = null;
// 循环执行的任务逻辑
function taskLoop() {
while (running) {
// 执行手势
gesture(500, [576, 1360], [555, 865]);
// 更新 UI 必须在 ui.run 中
ui.run(function () {
toastLog("执行一次任务");
});
// 随机延迟 1-5 秒
var delay = Math.ceil(Math.random() * 5) * 1000;
ui.run(function () {
toastLog("下一次间隔为:" + delay);
});
// 子线程中使用 sleep 而不是 setTimeout 进行等待
sleep(delay);
}
}
// 悬浮窗界面
var w = floaty.window(
<vertical padding="8" bg="#bababab4">
<text id="running">停止中</text>
<button id="start" text='开始' />
<button id="btn1" text='停止' />
<button id="btn3" text='退出' />
</vertical>
);
// 开始按钮
w.start.click(function () {
if (running) {
toastLog("任务已经在执行!");
return;
}
running = true;
// 更新 UI
ui.run(function () {
w.running.setText("运行中");
});
toastLog("任务启动成功");
// 启动子线程执行任务
taskThread = threads.start(function () {
// 延迟 2 秒后开始
sleep(2000);
taskLoop();
});
});
// 停止按钮
w.btn1.click(function () {
if (!running) {
toastLog("没有任务在运行!");
return;
}
running = false;
w.running.setText("停止中");
toastLog("任务已停止");
});
// 退出按钮
w.btn3.click(function () {
running = false;
w.close();
engines.myEngine().forceStop();
});
// 悬浮窗基础设置
w.setAdjustEnabled(true);
w.setPosition(100, 100);
w.setSize(-2, -2);
// 脚本保活
setInterval(function () {}, 60000 * 60);
主要改动说明
- 线程分离:使用
threads.start()包裹taskLoop函数。这样gesture()和sleep()在子线程运行,不会卡住悬浮窗界面。 - UI 安全:所有涉及
w.running.setText()或toastLog的操作都放入了ui.run()中,防止子线程操作 UI 导致崩溃。 - 循环优化:将递归的
setTimeout改为while(running)循环配合sleep()。这是自动化脚本的标准写法,更稳定且易于控制停止状态。 - 语法修正:将所有箭头函数
()=>替换为function(),兼容 Rhino 引擎,避免潜在的语法错误。
按照上述修改后,点击开始按钮不会再导致软件卡死。