|
|
定时器驱动的采集数据报表闭环
- /**
- * 案例26:定时器驱动的采集数据报表闭环
- *
- * 三部曲(24→25→26)第三篇:数据流水线**自己转**。
- * 案例24/25 的"统计+导出"靠主循环 while+sleep 推动,脚本一停就断;
- * 本案例换成 runTime.setInterval:注册两个定时器后主循环纯待机——
- * ⏱ 每 15 秒:四库统计任务(总条数/命中数,一行日志)
- * ⏱ 每 45 秒:CSV 导出任务(控件明细带 BOM+转义落盘 /sdcard/报表/)
- * 两个任务各干各的,第 3 次统计时导出刚好到期——"流水线节拍"演示。
- *
- * 教学对照(事件驱动三部曲收口):
- * 案例20~25 主循环 = 脚本**自己踩点**(while+sleep,停了就断);
- * 案例21 = **系统事件**驱动(通知来了才动);
- * 案例26 = **自建定时器**驱动(setInterval 注册后主循环彻底放手)——
- * 挂机脚本的标准形态:初始化一次,之后全在回调里,主线程只负责心跳和退出。
- *
- * 新知识点:
- * 1. runTime.setInterval(函数, 毫秒) 返回定时器 id;runTime.clearInterval(id) 停表
- * (生产脚本 mgkj.js 心跳/日志刷新的同款用法);
- * 2. **回调里必须整体 try/catch**:定时器回调抛异常没兜住,表可能直接停摆,
- * 主循环却在傻等——挂机脚本半夜断流的经典成因;
- * 3. 全局计数器(统计次数/导出次数)在回调里++、在主循环心跳里读——
- * 回调和主循环共享同一套全局变量,这就是"模块间通信最省事的办法";
- * 4. 停止顺序:置标志 → clearInterval 全部定时器 → stopAllRuning 放最后
- * (不 clear 的话脚本停了回调还可能被触发一次,清理逻辑跑在已关的库上)。
- *
- * 前置:先跑案例20~23 攒数据(没数据也能跑,统计全 0,导出跳过——容错演示)。
- */
- // ========== 配置区(改这里) ==========
- var 统计周期毫秒 = 15000; // 每 15 秒一轮四库统计
- var 导出周期毫秒 = 45000; // 每 45 秒一次 CSV 导出(统计的 3 倍节拍)
- var CSV最大行数 = 200;
- var 报表目录 = "/sdcard/报表";
- var 明细库路径 = "/sdcard/控件采集.db";
- var 明细表名 = "ui_records";
- var 采集库列表 = [
- { 名称: "OCR", 路径: "/sdcard/屏幕文字.db", 表名: "ocr_records" },
- { 名称: "通知", 路径: "/sdcard/通知消息.db", 表名: "notify_records" },
- { 名称: "剪贴板", 路径: "/sdcard/剪贴板内容.db", 表名: "clipboard_records" },
- { 名称: "控件", 路径: "/sdcard/控件采集.db", 表名: "ui_records" }
- ];
- // ========== 全局变量顶部声明 ==========
- var gStop = false;
- var 统计定时器id = null;
- var 导出定时器id = null;
- var 累计统计次数 = 0; // 回调里++、心跳里读——全局变量就是回调与主循环的通信总线
- var 累计导出次数 = 0;
- var 最近一轮统计 = "暂无"; // 缓存最近统计结果,心跳日志里回放
- // ========== importClass 只能放最外层 ==========
- importClass(android.database.sqlite.SQLiteDatabase);
- importClass(java.io.File);
- importClass(java.io.FileWriter);
- importClass(java.text.SimpleDateFormat);
- // 踩坑:java.util.Date 不能 importClass(和 JS 内置 Date 撞名,Rhino 拒绝导入),用全限定名
- // ========== 工具函数(先定义后调用) ==========
- // CSV 字段转义(案例25 同款:含逗号/引号/换行必须引号包裹+引号翻倍)
- function CSV转义(值) {
- var s = "" + 值;
- if (s.indexOf(",") >= 0 || s.indexOf(""") >= 0 || s.indexOf("\n") >= 0 || s.indexOf("\r") >= 0) {
- return """ + s.replace(/"/g, """") + """;
- }
- return s;
- }
- // 统计任务(定时器1 的回调):四库 COUNT 一行汇总——整体 try/catch,踩坑点2
- // 实测大坑:停止/重跑脚本后,旧实例的 setInterval 可能没被清掉继续跑(日志里会看到
- // 别的脚本运行期间冒出"统计任务#N"、计数跳号)——回调第一行必须查 gStop 闸门自杀
- function 统计任务() {
- if (gStop) {
- try { if (统计定时器id != null) runTime.clearInterval(统计定时器id); } catch (eStop) {}
- return;
- }
- try {
- 累计统计次数++;
- var 片段 = [];
- var 总条数 = 0;
- for (var i = 0; i < 采集库列表.length; i++) {
- var 库 = 采集库列表[i];
- var f = new File(库.路径);
- if (!f.exists()) { 片段.push(库.名称 + "×无库"); continue; }
- var db = null;
- try {
- db = SQLiteDatabase.openDatabase(库.路径, null, SQLiteDatabase.OPEN_READONLY);
- var c = db.rawQuery("SELECT COUNT(*) FROM " + 库.表名, null);
- var n = 0;
- if (c.moveToNext()) n = c.getInt(0);
- c.close();
- 总条数 = 总条数 + n;
- 片段.push(库.名称 + "×" + n);
- } catch (e库) {
- 片段.push(库.名称 + "×异常");
- } finally {
- if (db != null) { try { db.close(); } catch (e2) {} }
- }
- }
- 最近一轮统计 = 片段.join(" | ") + " | 合计 " + 总条数 + " 条";
- printl("📈 [统计任务#" + 累计统计次数 + "] " + 最近一轮统计);
- } catch (e) {
- // 踩坑点2:回调抛异常不兜住,定时器可能停摆,主循环却还在傻等
- printl("⚠ 统计任务异常(已兜住,定时器继续跑): " + e);
- }
- }
- // 导出任务(定时器2 的回调):控件明细 CSV(BOM 头,时间戳文件名)——同样 gStop 闸门+try/catch
- function 导出任务() {
- if (gStop) {
- try { if (导出定时器id != null) runTime.clearInterval(导出定时器id); } catch (eStop) {}
- return;
- }
- try {
- var f = new File(明细库路径);
- if (!f.exists()) {
- printl("ℹ [导出任务] 明细库不存在(案例23 没跑过),本轮跳过");
- return;
- }
- 累计导出次数++;
- var db = null;
- try {
- db = SQLiteDatabase.openDatabase(明细库路径, null, SQLiteDatabase.OPEN_READONLY);
- var c = db.rawQuery("SELECT page, text, class_name, cx, cy, collected_at FROM " +
- 明细表名 + " ORDER BY id DESC LIMIT " + CSV最大行数, null);
- var 行们 = [];
- 行们.push("页面,控件文字,类名,中心X,中心Y,采集时间");
- var fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- var 行数 = 0;
- while (c.moveToNext()) {
- 行数++;
- 行们.push(CSV转义(c.getString(0)) + "," + CSV转义(c.getString(1)) + "," +
- CSV转义(c.getString(2)) + "," + CSV转义(c.getString(3)) + "," +
- CSV转义(c.getString(4)) + "," + CSV转义(fmt.format(new java.util.Date(c.getLong(5)))));
- }
- c.close();
- var 路径 = 报表目录 + "/定时明细_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new java.util.Date()) + ".csv";
- var ff = new File(路径);
- if (ff.getParentFile() != null && !ff.getParentFile().exists()) ff.getParentFile().mkdirs();
- var writer = new FileWriter(ff);
- try {
- writer.write("\uFEFF" + 行们.join("\r\n") + "\r\n"); // BOM 第一个写,Excel 中文不乱码
- writer.flush();
- } finally {
- writer.close();
- }
- printl("💾 [导出任务#" + 累计导出次数 + "] " + 行数 + " 行 → " + 路径 + " (" + ff.length() + " 字节)");
- } finally {
- if (db != null) { try { db.close(); } catch (e3) {} }
- }
- } catch (e) {
- printl("⚠ 导出任务异常(已兜住,定时器继续跑): " + e);
- }
- }
- // ========== 主流程 ==========
- printl("===== 案例26 定时器驱动报表闭环 启动 =====");
- printl("⚙ 配置:统计每 " + (统计周期毫秒 / 1000) + " 秒 · 导出每 " + (导出周期毫秒 / 1000) + " 秒 · 库: " + 采集库列表.length + " 个 · 报表: " + 报表目录);
- printl("ℹ 三部曲第三篇:流水线自己转——注册完 setInterval 主循环彻底放手,任务全在回调里跑");
- printl("ℹ 对照:案例20~25 主循环自己踩点(while+sleep) | 案例21 系统事件驱动 | 本案例自建定时器驱动(挂机脚本标准形态)");
- // 注册定时器(返回 id,停止时 clearInterval 用)
- 统计定时器id = runTime.setInterval(统计任务, 统计周期毫秒);
- 导出定时器id = runTime.setInterval(导出任务, 导出周期毫秒);
- printl("⏱ 两个定时器已注册:统计 id=" + 统计定时器id + " | 导出 id=" + 导出定时器id);
- printl("ℹ 注册完就没人管了——下面主循环只做心跳,每 10 秒回放最近统计(挂机脚本形态)");
- // 停止按钮清理(顺序:置标志 → 停全部定时器 → stopAllRuning 放最后;
- // 不先 clearInterval,脚本停了回调还可能被触发,清理逻辑跑在已关的库上)
- event.onStopBtnEvent(function () {
- gStop = true;
- try { if (统计定时器id != null) runTime.clearInterval(统计定时器id); } catch (e1) {}
- try { if (导出定时器id != null) runTime.clearInterval(导出定时器id); } catch (e2) {}
- runTime.stopAllRuning();
- });
- // 心跳主循环:只读全局变量打进度,真正的活在两个回调里干
- var 心跳计数 = 0;
- while (!gStop) {
- 心跳计数++;
- printl("💤 心跳#" + 心跳计数 + " | 统计跑了 " + 累计统计次数 + " 轮 | CSV 导了 " + 累计导出次数 + " 版 | 最近统计: " + 最近一轮统计);
- sleep.second(秒 = 10);
- }
- printl("===== 流程结束:统计 " + 累计统计次数 + " 轮,导出 " + 累计导出次数 + " 版,定时器已清理 =====");
复制代码
|
|