news 2026/10/17 5:12:41

AutoJs源码实战:用Shell命令操作SQLite数据库的可靠方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AutoJs源码实战:用Shell命令操作SQLite数据库的可靠方案

简介:AutoJs源码项目聚焦于通过shell命令操作SQLite数据库这一具体场景,面向需要在安卓自动化脚本中读写本地数据库的开发者,常见于本地配置缓存、离线数据存储与简单日志记录等需求。资源是可直接运行的Auto.js项目模板,兼容低版本AutoJs,导入后即可查看实际执行效果。压缩包仅含1个js文件,整包约775B,短小精悍;代码中演示了sqlite3命令的完整用法,包括通过shell传递数据库路径与SQL语句、处理特殊字符转义、执行建表与增删改查,以及解析命令输出等关键步骤,配合注释可理解命令调用与结果反馈的交互逻辑,方便直接借鉴或封装成公共函数。目前已有122人学习下载,适合初入AutoJs、想通过命令方式操作数据库的开发者参考。利用该示例可快速搭建数据库操作基础,将其嵌入自己的自动化脚本,既能减少依赖,又能省去从零调试shell命令的摸索时间。

1. AutoJs源码里的shell操作sqlite数据库:为什么我不用内置模块,而是让命令行走前面

接手第一份自动记录手机使用情况的AutoJs源码时,我第一反应是找内置的sqlite模块,结果真机上反复遇到锁库、数据难复用和错误信息不直观的问题。后来我改成用shell操作sqlite数据库,把sqlite3命令行当一个外部工具来调,不要折腾模块封装,SQL怎么写数据库就怎么执行,调试时还能在终端里单独复现。这套AutoJs源码里真正值钱的不是那一句exec,而是两层引号转义和对结果行的稳定解析,适合要定时落库、要把数据再交给其它脚本或应用处理的自动化场景。

2. 先立住原理:AutoJs里的Shell通道和sqlite3命令是怎么配合的

2.1 手机上的sqlite3命令:先确认设备里有这个可执行文件

shell操作sqlite数据库,前提是Android系统里有一个能独立运行的sqlite3二进制,AutoJs只是负责把SQL文本交给它。绝大多数原生和类原生ROM会把sqlite3放在/system/xbin或/system/bin下,但部分定制ROM会裁掉它,所以第一步不是写AutoJs,而是先确认环境。

which sqlite3 sqlite3 --version

第一行返回路径说明命令在PATH里;如果没有任何输出,再检查/system/xbin/sqlite3和/system/bin/sqlite3是否存在。

ls -l /system/xbin/sqlite3 /system/bin/sqlite3

如果两个路径都没有,后面的脚本再怎么写都跑不起来。这时候不要急着找第三方二进制,先换回AutoJs内置的sqlite模块,或者把数据库操作改成读文件再交给其它程序处理,这是成本最低的兜底方案。设备上存在sqlite3但版本太老也不要慌,后面用到的建表、insert、select都是十几年前就稳定的语法,不需要新版特性。

2.2 为什么用Shell而不是内置sqlite模块

题目标题里点名shell,说明作者已经踩过内置模块的边界。我的对比表里,Shell方案的优势集中在数据复用和排错这两件事上。

维度内置模块方案Shell调用sqlite3
锁库处理进程内持锁,外部工具访问容易冲突可以命令行单独验证锁
数据复用脚本结束数据还在,但对外部工具不透明CLI输出是纯文本,任何工具都能读
排错错误信息封装在模块返回里,要看黑匣子拆成sqlite3命令后可以直接复制执行
加密数据库默认不透明手机里的sqlite3可以对接加密库

注意这里不是否定内置模块,它的类型转换和线程安全更省事,适合单脚本自产自销。但只要脚本还要做数据备份、导出、给其它展示页用,Shell的可复现性就远比封装接口值钱。

2.3 能跑通的最小命令:new Shell加一条exec

AutoJs里执行shell的最简写法,其实就三件套:new Shell、exec、exit。但有两个参数容易错。

auto.waitFor(); var sh = new Shell(true); var dbPath = "/data/local/tmp/autojs.db"; var res = sh.exec("sqlite3 " + dbPath + " \"create table if not exists t(id integer primary key, name text);\""); sh.exit(); if (res.code === 0) { console.log("建表成功"); } else { console.error(res.result); }

new Shell(true)的true表示以root方式执行,没有root时写数据库基本都会失败;exec是同步的,卡住会阻塞脚本,所以尽量不要把几十万行SQL塞进一条命令;返回值res.code是退出码,0表示成功,res.result是命令输出。命令末尾固定调用sh.exit()是很多源码容易漏的,Shell进程不释放会占用一个su会话。

这条命令里SQL用双引号包住,是一个非常重要的约定。Android的sh用双引号包字符串时,SQL内部的单引号不会被shell解释,可以正常传给sqlite3作为文本字面量;反过来如果直接用单引号包整个SQL,SQL里的单引号就会提前把shell引号截断。后面所有增删改查都沿用这个约定,可以减少一半的转义问题。

双引号也不是百分百安全,SQL里一旦出现$或反引号会被shell展开,这个问题留到排查章节处理。

2.4 封装一个最常用的执行函数

每次操作都new Shell会显得很啰嗦,我一般会在AutoJs源码文件顶部放一个dbExec函数,后续所有操作都走它。

var DB_PATH = "/data/local/tmp/autojs.db"; function dbExec(sql) { var sh = new Shell(true); var cmd = "sqlite3 -batch " + DB_PATH + " \"" + escapeShellDoubleQuote(sql) + "\""; var res = sh.exec(cmd); sh.exit(); if (res.code !== 0) { throw new Error(res.result); } return res.result; } function escapeShellDoubleQuote(s) { return s .replace(/\\/g, "\\\\") .replace(/"/g, '\\"') .replace(/\$/g, "\\$") .replace(/`/g, '\\`'); }

-batch参数让sqlite3不进入交互模式,比较适合脚本调用。escapeShellDoubleQuote只解决shell层的双引号转义,不负责SQL层的单引号转义,两层要分开写,合在一起后面会非常难排查。把dbPath定义在顶层还有一个好处:以后要切换数据库文件,或者改成读取配置文件,不需要翻整个脚本改路径。

3. 把增删改查搬到Shell上:SQL构造与结果解析的五个细节

3.1 建表之前先处理目录

sqlite3在Android上如果数据库文件所在的目录不存在,它不会像桌面Linux那样自动创建父目录,而是直接报错。所以建表的第一步是确保目录存在。

var dbDir = "/data/local/tmp"; var dbPath = dbDir + "/app_record.db"; var initSql = [ "create table if not exists app_log(", " id integer primary key autoincrement,", " app text not null,", " ts integer not null", ");" ].join(" "); var cmd = "mkdir -p " + dbDir + " && sqlite3 " + dbPath + " \"" + initSql + "\""; var sh = new Shell(true); var res = sh.exec(cmd); sh.exit(); if (res.code !== 0) { console.error(res.result); }

这里的mkdir -p同时解决了两个问题:目录不存在就创建,目录已存在也不会报错。dbDir用/data/local/tmp是因为它是shell用户和root都习惯使用的临时目录,比/sdcard少一层存储挂载权限,也不会因为外部存储没有挂载而失败。create table里加了if not exists,整段initSql可以反复执行,后面再接定时任务时不需要额外判断表是否存在。

如果要把数据长期放在/sdcard下,注意Android的受保护存储可能让sqlite3进程拿到错误路径,我建议先用上面的/data/local/tmp验证逻辑,再决定要不要迁移。

3.2 插入数据:SQL单引号和Shell双引号要分层处理

插入语句是翻车率最高的一块,难点不在SQL本身,而在一个文本值要经过两层解析:先被AutoJs生成字符串,再被shell解析,最后才交给sqlite3。很多人只转义Shell层,忘了SQL层,于是遇到中文或者带单引号的包名就报错。

SQL层的规则:字符串常量用单引号包裹,文本里出现的单引号要写成两个单引号。

function sqlQuote(v) { return "'" + String(v).replace(/'/g, "''") + "'"; } function dbExec(sql) { var cmd = "sqlite3 -batch " + DB_PATH + " \"" + escapeShellDoubleQuote(sql) + "\""; // 省略 Shell 打开和退出 } var app = "某聊天App"; var ts = Date.now(); var sql = "insert into app_log(app, ts) values(" + sqlQuote(app) + ", " + ts + ");"; dbExec(sql);

当你调用sqlQuote("某聊天App")时,得到的是'某聊天App',这个字符串里不存在shell敏感字符,所以escapeShellDoubleQuote不会改动它。如果app是"O'Brien",sqlQuote先把它变成'O''Brien',这是sqlite3能正确读取的文本形式。不能反过来只做shell转义,因为shell双引号里的单引号本来不会被识别,SQL层不转义的话sqlite3会把字符串切到中间的引号处。

数值不需要被sqlQuote包住,ts直接用Date.now()的数字拼进SQL,这也是插入语句里唯一不需要转义的值。

3.3 查询结果:用不可见分隔符替代默认的竖线

sqlite3命令行默认用竖线分隔列,用换行分隔行。粗看够用,但真实业务文本里经常出现竖线,比如备注"a|b",解析时就会多出一列。我一般不在查询时依赖默认格式,而是用SQL拼一个SELECT列。

function queryRows(sql) { var sep = String.fromCharCode(1); var safeSql = sql.replace(/\$/g, "\\$").replace(/"/g, '\\"'); var sh = new Shell(true); var res = sh.exec("sqlite3 -batch " + DB_PATH + " \"" + safeSql + "\""); sh.exit(); if (res.code !== 0) { throw new Error(res.result); } var lines = res.result.split("\n"); var rows = []; var i, cols; for (i = 0; i < lines.length; i++) { if (!lines[i].trim()) continue; cols = lines[i].split(sep); rows.push({ app: cols[0], ts: Number(cols[1]) }); } return rows; } var list = queryRows("select ifnull(app, '') || char(1) || ifnull(ts, 0) from app_log");

String.fromCharCode(1)是一个不可见控制字符,正常业务文本几乎不会出现。SQL里写char(1)表示同一个分隔符,JS里用String.fromCharCode(1)再去分列,两端必须一致。ifnull是个容易忽略的点:SELECT里如果直接用app || char(1) || ts,而app为NULL,整个表达式的值是NULL,sqlite3输出一行空文本,JS端就会漏掉这条记录。给每个字段包一层ifnull,才能保证每一行都有可解析的列数。

如果是纯数字查询,不需要这么谨慎,但凡是面向文本字段的通用查询函数,我都建议保留这个写法。

3.4 更新与删除:WHERE条件同样要走sqlQuote

更新和删除最常见的错误是只处理set子句的文本,WHERE里的字符串直接拼接。SQL层转义缺了,条件里的引号会让整条语句提前结束。

var whereApp = "某聊天App"; var remark = "已处理"; var sql = "update app_log set remark = " + sqlQuote(remark) + " where app = " + sqlQuote(whereApp) + " and ts > " + 1700000000000; dbExec(sql);

这段代码的where app = '某聊天App'是文本比较,所以whereApp必须用sqlQuote;ts是数值比较,直接写数值和阈值。update语句里,set子句的remark同样是字符串,也由sqlQuote处理,不存在“只有where才需要转义”的说法。

删除语句完全一样:

var sql = "delete from app_log where app = " + sqlQuote("某聊天App") + " and ts < " + 1700000000000; dbExec(sql);

这里的and是SQL关键字,在shell双引号内不会被当成shell的&&,所以不需要额外处理。真正需要担心的是整个SQL字符串里的双引号,如果有字段名用了双引号标识符,建议改成方括号或者反引号,避免和shell双引号冲突。

4. 一个完整示例:用AutoJs源码定时记录前台应用

4.1 需求与表结构

这个示例解决一个很常见的诉求:统计自己到底在哪些App上花了多少时间。采集过程就是每隔一秒取一次当前前台应用包名,发现切换就记一条;数据持续写入本地sqlite,最后再用查询函数算每个包的停留区间。

表结构只需要四个字段:自增id、包名、时间戳、备注。

create table if not exists app_log( id integer primary key autoincrement, app text not null, ts integer not null, remark text );

remark一开始不填,留给后面的补录操作。ts存的是毫秒时间戳,方便跨天和按小时分段聚合。

4.2 主循环与批量落库源码

日志型任务不能每秒钟调用一次sqlite3,每次exec都会启动一个新进程,一秒一条会让CPU和IO都很难看。做法是先把切换记录攒在内存数组里,每五分钟用一条带事务的SQL文件整体写入。

auto.waitFor(); var DB_PATH = "/data/local/tmp/app_usage.db"; var SQL_FILE = "/data/local/tmp/pending.sql"; var FLUSH_MS = 5 * 60 * 1000; var sh = new Shell(true); var buffer = []; var lastPkg = ""; var lastFlush = Date.now(); function sqlQuote(v) { return "'" + String(v).replace(/'/g, "''") + "'"; } function initDb() { var cmd = "mkdir -p /data/local/tmp && sqlite3 " + DB_PATH + " \"create table if not exists app_log(id integer primary key autoincrement, app text not null, ts integer not null, remark text);\""; sh.exec(cmd); } function flushBuffer() { if (buffer.length === 0) return; var lines = ["begin;"]; var i, item; for (i = 0; i < buffer.length; i++) { item = buffer[i]; lines.push("insert into app_log(app, ts, remark) values(" + sqlQuote(item.app) + ", " + item.ts + ", null);"); } lines.push("commit;"); var cmd = "cat > " + SQL_FILE + " <<'SQLEOF'\n" + lines.join("\n") + "\nSQLEOF\n"; cmd += "sqlite3 " + DB_PATH + " < " + SQL_FILE; var res = sh.exec(cmd); if (res.code !== 0) { console.error(res.result); } else { buffer = []; } } initDb(); while (true) { var pkg = currentPackage(); if (pkg && pkg !== lastPkg) { buffer.push({ app: pkg, ts: Date.now() }); lastPkg = pkg; } if (Date.now() - lastFlush > FLUSH_MS) { flushBuffer(); lastFlush = Date.now(); } sleep(1000); } sh.exit();

这段的核心是flushBuffer里的heredoc:把整个事务写进临时文件,再用重定向喂给sqlite3。SQL里有单引号、中文、换行都不需要再跟shell的双引号较劲,因为heredoc的结束标记SQLEOF用了单引号包裹,shell不会展开内容。begin和commit包住所有insert,保证要么全成功要么全失败,不会出现写了一半、下次启动时重复插入。

循环里的currentPackage是AutoJs提供的前台包名获取接口,拿不到时返回空,所以判断里先pkg再pkg !== lastPkg。sleep(1000)控制采样频率,一分钟切换次数多的场景可以改成500,但不建议低于500,否则currentPackage的调用开销会把电池拖垮。

buffer在一轮flush成功后立刻清空,这是防止重复写的关键。flush失败时不清理,下一轮再重试,等于给了自己后悔药。

4.3 数据验证:把落库结果读回来

写完了要能读回来,否则就是黑匣子。验证时不要看控制台打出来的插入是否成功,直接查总行数和最后几条。

function query(sql) { var file = "/data/local/tmp/check.sql"; var cmd = "cat > " + file + " <<'SQLEOF'\n" + sql + "\nSQLEOF\n"; cmd += "sqlite3 -batch " + DB_PATH + " < " + file; var res = sh.exec(cmd); if (res.code !== 0) throw new Error(res.result); return res.result; } console.log(query("select count(*) from app_log;")); console.log(query("select app, ts from app_log order by ts desc limit 5;"));

这段在脚本末尾跑,输出里能看到总行数和最近五条记录。如果总行数是0,回到主循环检查buffer是否真的积累了数据,常见原因是pkg一直是空,currentPackage需要屏幕开启和辅助功能权限。这步能区分问题出在采集还是落库。

5. 常见问题排查:Shell操作sqlite数据库的5个坑

5.1 报错“sqlite3: not found”:二进制缺失或PATH没带上

现象是exec返回127,res.result里写sqlite3: not found。原因有两个:ROM没带sqlite3,或者PATH里没有sqlite3所在目录。

解决:先执行which sqlite3,没有输出再看/system/xbin/sqlite3是否存在。若存在但AutoJs还是找不到,把命令改成绝对路径:

sh.exec("/system/xbin/sqlite3 " + DB_PATH + " \".tables\"");

若绝对路径也不存在,就不要在这个命令上死磕,换内置sqlite模块或把数据落到JSON文件,都比去找一个不透明的二进制靠谱。

5.2 “attempt to write a readonly database”:目录权限和SELinux一起捣乱

现象是建表和插入都报readonly database,但读取正常。原因通常是数据库文件所在目录对当前shell用户不可写,或者SELinux拦截了写入。

解决:优先把数据库放到shell用户可写的目录,比如/data/local/tmp,这是整篇文章一致推荐的位置。如果因为业务原因必须放/data/data/包名/databases,先用root执行chown和chmod把权限放开:

chown shell:shell /data/data/example/databases/app.db chmod 600 /data/data/example/databases/app.db

如果还是readonly,把SELinux临时设为Permissive验证,但别在生产脚本里长期关。

5.3 SQL里中文乱码:先怀疑源文件编码,再怀疑shell

现象是在AutoJs日志里,中文变成???或乱码。原因有两个:AutoJs源码文件本身不是UTF-8保存,常见于Windows上默认GBK存储;少数设备sqlite3终端的显示编码不是UTF-8。

解决:源码文件统一保存为UTF-8无BOM,这是最常见的修复。如果保存没问题,用sqlite3的hex输出看底层内容:

sqlite3 db "select hex(remark) from app_log limit 1;"

如果hex是正常的UTF-8序列,说明落库正确,只是显示层乱码,不用处理。

5.4 SQL里的$和反引号被Shell吃掉

现象是插入文本包含$符号,落库后字段变短或变成空。原因是我在2.3提过的双引号约定:shell双引号内$会触发变量展开,反引号会执行命令替换。

解决:如果你坚持用双引号拼SQL,就要写完整的escapeShellDoubleQuote函数,把$和反引号都替换成转义写法。更稳妥的是用第4章的heredoc文件方案,SQL内容里有什么就写什么,完全不经过shell解析。

var bad = "价格$99"; var sql = "insert into t(name) values(" + sqlQuote(bad) + ");"; var cmd = "cat > /data/local/tmp/s.sql <<'SQLEOF'\n" + sql + "\nSQLEOF\n"; cmd += "sqlite3 " + DB_PATH + " < /data/local/tmp/s.sql"; sh.exec(cmd);

这个办法治本,也是我现在默认的写库方式。

5.5 查询结果里NULL让整行消失

现象是查询函数返回的行数比表里实际行数少,而且丢的恰好是某字段没填的记录。原因是SELECT里用||拼接字段时,任一字段为NULL,整条表达式就为NULL,sqlite3命令行输出一个空串或什么都不输出。

解决:每个参与拼接的字段都用ifnull包一层,数值给默认0,文本给默认空串:

select ifnull(app, '') || char(1) || ifnull(ts, 0) from app_log;

这个坑不只在Shell方案里有,AutoJs内置模块同样存在,但命令行方案里整行被吞的现象更隐蔽,因为返回值里没有报错。

6. 进阶技巧:批量写入提速与把查询结果转成JSON

6.1 一个函数搞定批量事务

日志型脚本最容易出现的性能问题是for循环里每一条都调dbExec,等于每一行都启动一次sqlite3进程。我常用的阈值是500条一事务:

function batchInsert(rows) { var lines = ["begin;"]; var i; for (i = 0; i < rows.length; i++) { lines.push("insert into app_log(app, ts) values(" + sqlQuote(rows[i].app) + ", " + rows[i].ts + ");"); } lines.push("commit;"); var file = "/data/local/tmp/batch.sql"; var cmd = "cat > " + file + " <<'SQLEOF'\n" + lines.join("\n") + "\nSQLEOF\n" + "sqlite3 " + DB_PATH + " < " + file; return sh.exec(cmd).code === 0; }

一次事务写入500条,比500次单条exec快一个量级。别把事务开得过大,临时文件会占内存,而且中途失败时回滚整个大事务反而更难定位。

6.2 给查询加一层JSON包装

AutoJs脚本里经常要带着查询结果调外部接口,把命令行输出转成JSON会更省事:

function queryJson(sql) { var rows = queryRows(sql); return JSON.stringify(rows); }

queryRows就是第3.3节的函数,它返回对象数组,JSON.stringify直接可用。字段名和类型在queryRows里已经定死,不需要在调用处再解析文本。

6.3 我的习惯

这套Shell方案我用了快半年,最大的教训是:不要在exec字符串里硬拼一段很长的SQL,SQL只写进临时文件,shell只负责把文件喂给sqlite3。这样每次写新脚本时都不用再回忆转义规则,也顺手把第5章一半的坑绕开了。如果你要接手别人的AutoJs源码,先看他有没有统一的dbExec或者flushBuffer,如果没有,大概率是到处new Shell的写法,趁早重构。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 5:12:33

2026云原生技术趋势:平台工程、FinOps与AI治理的落地实践

2026年初&#xff0c;在一次企业技术平台改造的交流中&#xff0c;某位负责基础架构的总监问了我一个很扎心的问题&#xff1a;“我们的订单、支付、库存系统全部完成容器化迁移了&#xff0c;Kubernetes 集群也建了不少&#xff0c;按理说云原生应该出成果了&#xff0c;可业务…

作者头像 李华
网站建设 2026/10/17 5:12:11

如何降低 Claude Code 的 Token 消耗:基于代码地图的精准上下文注入

1. 为什么 Claude Code 会越用越贵&#xff1a;Token 全被“上下文”吃掉了1.1 一次让人肉疼的账单经历先说个真实案例。我之前接手一个老仓库&#xff0c;代码量不算夸张&#xff0c;大概六万多行&#xff0c;但文件散落在几十个目录里。我用 Claude Code 做一次跨模块的登录鉴…

作者头像 李华
网站建设 2026/10/17 5:12:06

家庭用电预测:XGBoost+SHAP的可解释时序建模实践

简介&#xff1a;本资源是一份面向机器学习初学者与实践者的家庭用电预测实战项目&#xff0c;聚焦回归算法在能源数据分析中的典型应用&#xff0c;适用于课程设计、课设实践或Kaggle类入门建模任务。压缩包共4个Python源文件&#xff08;.py&#xff09;&#xff0c;总大小仅…

作者头像 李华
网站建设 2026/10/17 5:11:51

AI硬件交互设计:打破语音唯一论,多模态融合实战指南

上个月在一家智能硬件孵化器做了一轮非正式评测&#xff0c;桌上摆着八台不同团队的AI硬件原型&#xff0c;有做桌面助手的&#xff0c;有做随身记录器的&#xff0c;还有做智能相框的。按下测试按钮的那一刻&#xff0c;房间里此起彼伏的唤醒词让我突然意识到一个问题&#xf…

作者头像 李华
网站建设 2026/10/17 5:11:41

TFS团队项目配置详解:PATFS源码分析从零到实战

我最近在折腾 TFS 源码分析&#xff0c;手里的主力工具是 PATFS&#xff0c;第一步就是配置团队项目。说实话&#xff0c;这一步卡住过不少人&#xff0c;尤其是第一次接触 TFS 的新手&#xff0c;完全分不清"团队项目"和"项目集合"到底什么关系&#xff0…

作者头像 李华
网站建设 2026/10/17 5:11:26

清扫机器人强化学习:10项复合奖励函数让训练从100分涨到778分

前阵子我把清扫机器人项目的强化学习训练分数从 100 分抬到了 778 分&#xff0c;网络结构没动&#xff0c;算力没加&#xff0c;甚至训练集都没扩大&#xff0c;唯一的变量是奖励函数。这件事让我彻底相信&#xff1a;在复杂的机器人任务里&#xff0c;奖励工程的优先级&#…

作者头像 李华