简介AutoJs源码——用shell操作sqlite数据库是一份演示如何在AutoJs自动化脚本中通过shell命令操作SQLite数据库的实战源码面向需要处理本地数据、批量读取或写入SQLite的AutoJs学习者与脚本开发者。整个资源以7z压缩包形式提供共1个文件即1个可直接运行的js脚本压缩后大小仅775B源码按实际项目模板封装安装AutoJs后运行即可打开省去繁琐配置尤其对低版本AutoJs友好。通过学习该脚本可以理解shell与sqlite3配合完成数据库建表、查询、更新等常见操作的思路获得一个轻量、可改造的数据库操作范例其简洁结构适合拆解为通用功能片段嵌入自动化流程或作为二次开发起步代码。已有121人学习/下载适合快速参考资源仅供学习与参考请勿用于商业用途使用前请确认已熟悉AutoJs的基本安装与运行方式。 先聊个现象。很多朋友在AutoJs里做数据持久化第一反应就是对着官方文档写SQLiteDatabase.openDatabase()再老老实实调insert、query方法封装一层又一层。其实在安卓设备上还有一条更直接的路直接在shell里敲sqlite3命令。这个组合——AutoJs负责把命令拼好、把结果收回来sqlite3负责真正干活——我在好几个自动化项目里都用得很顺今天把源码思路和踩过的坑一次性聊透。这个方案解决的是三类问题一是AutoJs内置SQLite API在多线程、大批量写入时表现不理想二是脚本状态、采集数据这种需要落库的场景用shell命令能直接在电脑上通过adb查看数据库内容调试极方便三是它不依赖AutoJs的框架版本只要有root权限或adb通道几乎任何安卓设备都能跑。适合做自动化脚本、数据采集、本地缓存类项目的朋友参考。1. 为什么偏偏要用shell去操作sqlite1.1 AutoJs自带SQLite API的几个不顺手的地方AutoJs确实提供了SQLite支持但上手之后你会发现几个痛点首先是API的整体设计偏Java风格写起来冗长try、catch、finally一个都不能少稍微复杂一点的查询就要写一堆回调其次是它在一些机型上对SQLiteDatabase的封装并不稳定尤其在Android 10以上分区存储权限收紧之后/data/data/包名/databases目录不是你想读就能读的更重要的是当数据量上来之后频繁的逐条插入性能很差动辄几秒才能完成一批写入。还有一个很隐蔽的问题AutoJs的SQLite API对SQL语句的错误提示并不友好。你拼了一条有问题的SQL它只给你抛一个模糊的异常排查起来非常头疼。而sqlite3命令行工具会把具体的语法错误和偏移位置直接打在终端里定位问题几乎不费劲。1.2 shell方案真正的优势在哪说到底shell sqlite3是绕过了所有中间层的直连方案。sqlite3是SQLite官方提供的命令行客户端你对数据库文件做的每一个操作它都在内核层面直接完成不存在API封装不一致的问题。查询结果返回的是纯文本拿到AutoJs里用字符串处理就搞定完全不用跟Java对象打交道。另一个实际好处可调试性极强。你在AutoJs脚本里跑不通的查询直接打开adb shell手动敲一遍同样的sqlite3命令马上就能看出是SQL写错了还是数据本身有问题。这在排查线上问题的时候简直是救命稻草。而且sqlite3自带的.mode csv、.import、.output等点命令让导出、清洗、批量导入数据变得异常简单几十万条数据的初始化只需要两三行命令。2. 环境准备先把shell通道打通2.1 检查设备上有没有sqlite3这个命令虽然在Android系统里sqlite3很常见但它并不是每个ROM都预装的。检查方法很简单在AutoJs里执行一条shell命令就行const shell require(shell); let res shell.execCommand(which sqlite3 || echo NOT_FOUND, true); log(res.result);如果输出的是/system/bin/sqlite3或者/system/xbin/sqlite3这类路径说明你的设备已经有sqlite3了。如果输出NOT_FOUND那就需要额外处理。常见做法有三种装一个BusyBox大多数BusyBox发行版自带sqlite3模块在Android模拟器或者某些定制ROM上直接apt install sqlite3仅限有包管理器的环境从SQLite官网下载Android适用的sqlite3二进制push到/data/local/tmp/再chmod 755赋权后使用。我自己最常用的是第三种因为这个二进制完全独立不依赖系统组件兼容性最好。需要注意存放sqlite3的目录必须在shell有执行权限/data/local/tmp是稳妥的选择。2.2 AutoJs执行shell的几种正确姿势AutoJs执行shell命令核心是Shell.execCommand方法但这里有个大坑它的第二个参数决定了是否使用root权限。如果你的设备已经root用true如果没root又想执行shell命令那基本走不通除非通过adb通道从电脑端执行。const shell require(shell); // 使用root权限执行 let res shell.execCommand(sqlite3 /sdcard/test.db \.tables\, true); if (res.code 0) { log(执行成功: res.result); } else { log(执行失败: res.result); }execCommand返回的是一个包含code和result的对象result就是命令的标准输出拼接结果。这个方法属于同步阻塞调用线程会卡住直到命令执行完。对于查询类的短命令没问题但如果是大批量导入这种耗时的操作建议放到threads子线程里执行否则AutoJs主界面会卡死。另外还有一个细节execCommand本质上是把整条命令交给shell去解析所以命令里的引号、空格、特殊字符都要格外小心。字符串拼接时路径和SQL语句最好都用双引号包起来防止路径中的空格把命令拆裂。3. 核心实操sqlite3命令行的完整用法与源码封装3.1 sqlite3命令行参数速查sqlite3命令行的基本结构是sqlite3 [数据库文件路径] [SQL语句]最简单的用法是直接执行一条SQL并退出sqlite3 /sdcard/test.db SELECT * FROM users;如果数据库文件不存在sqlite3会自动创建一个空的数据库文件这点和SQLite本身的行为一致。除了直接传SQL还有一类非常重要的点命令它们控制的是sqlite3客户端自身的输出格式和行为不是SQL语句点命令作用示例.tables列出所有表名sqlite3 db .tables.schema 表名查看建表语句sqlite3 db .schema users.headers on/off输出是否带列名sqlite3 -header db SELECT * FROM users;.mode column/list/csv设置输出格式sqlite3 -column db SELECT * FROM users;.output 文件名将结果重定向到文件sqlite3 db .output /sdcard/out.csv SELECT * FROM users;.import 文件名 表名从文件导入数据sqlite3 db .import /sdcard/data.csv users.timeout 毫秒设置锁等待超时sqlite3 db .timeout 5000 INSERT ...关于输出格式这里要多说两句。执行查询时如果带-header参数输出第一行就是列名后续每行是一条记录。用-column模式时列之间用空格对齐人眼看着友好但解析成结构化数据很容易出错——如果字段值里本身有空格列就切不干净。所以在AutoJs里我推荐用-separator指定一个特殊分隔符比如制表符再用split(\t)去解析这样最稳妥sqlite3 -header -separator $\t /sdcard/test.db SELECT * FROM users;3.2 在AutoJs源码里封装一套增删改查有了上面的基础就可以在AutoJs里封装自己的数据库操作层了。这里给出一套我实际在用的源码const shell require(shell); function runShell(cmd, useRoot true) { let res shell.execCommand(cmd, useRoot); if (res.code ! 0) { throw new Error(shell命令执行失败: res.result); } return res.result.trim(); } function execSql(dbPath, sql, useRoot true) { let cmd sqlite3 ${dbPath} ${sql.replace(//g, \\)}; return runShell(cmd, useRoot); } function query(dbPath, sql, useRoot true) { let cmd sqlite3 -header -separator $\\t ${dbPath} ${sql.replace(//g, \\)}; let out runShell(cmd, useRoot); if (!out) return []; let lines out.split(\n); let headers lines[0].split(\t); let rows []; for (let i 1; i lines.length; i) { let cols lines[i].split(\t); let obj {}; headers.forEach((h, idx) { obj[h] cols[idx] ! undefined ? cols[idx] : ; }); rows.push(obj); } return rows; }逐个解释一下这里的设计思路。execSql用于执行INSERT、UPDATE、DELETE、DROP等不返回结果集的SQL。注意这里的SQL语句里如果有双引号需要做一层转义否则会跟外层包SQL的引号冲突而SQL里值的单引号不需要转义因为外层用的是双引号。query用于查询输出时指定-header和-separator为制表符返回结果按行拆分第一行是列名后续行用split(\t)切成数组最后组装成对象数组。这套解析逻辑虽然简单但只要字段值里不出现制表符就非常可靠。如果字段值里确实可能包含制表符建议换成.mode csv配合逗号分隔原理一样只是分隔符换成逗号。批量插入时启动事务和提交事务可以用同一条SQL完成function batchInsert(dbPath, tableName, dataList, useRoot true) { if (!dataList || dataList.length 0) return; let rows dataList.map(row { let keys Object.keys(row); let vals keys.map(k { let v row[k]; if (typeof v string) { return v.replace(//g, ) ; } return v; }); return (${vals.join(, )}); }); let sql BEGIN; INSERT INTO ${tableName} VALUES ${rows.join(, )}; COMMIT;; execSql(dbPath, sql, useRoot); }dataList是一个对象数组每个对象代表一行数据。写值的时候字符串类型的值用一个单引号包住并把内部的单引号替换成——这是SQLite的规范转义方式。整批数据包在BEGIN和COMMIT之间一次提交性能比逐条INSERT高出一个数量级。4. 常见问题与排查技巧实录4.1 设备提示sqlite3: not found这个问题的本质是sqlite3二进制没有出现在shell的PATH环境变量里。你先用which sqlite3确认如果找不到别急着装先看看/system/xbin和/system/bin里有没有有时候只是PATH没包含。都没有的话就要么装BusyBox要么push一个单独的sqlite3二进制。这里有个易踩的坑就算你把二进制的权限设成755放在/data/local/tmp下如果分区挂载的是noexec照样执行不了会报Permission denied。遇到这种情况就把二进制放到/data/data/你的包名/files目录下这个目录通常有执行权限。4.2 unable to open database file这句报错很常见但原因有好几种。第一种是数据库文件所在目录不存在sqlite3不会自动创建多级目录得先mkdir -p第二种是权限不够对/data/data目录下的文件即使有root权限某些SELinux策略也会拦你处理办法是执行前先把SELinux临时设成permissive模式或者确保你的root权限是完整的第三种是路径里有空洞或特殊字符命令拼接时被shell拆散了。排查建议先ls -l看文件在不在再用stat看目录权限最后再检查命令本身的引号。4.3 database is locked并发写导致的锁冲突SQLite允许多个进程同时读但同一时刻只允许一个进程写。如果你的AutoJs脚本里开了多个线程同时往数据库里写就会频繁触发database is locked。解决思路有两个层面应用层面给所有写操作加一个线程锁或互斥队列让它们串行执行数据库层面开启WAL模式和设置busy timeoutsqlite3 test.db PRAGMA journal_modeWAL; PRAGMA busy_timeout5000;WAL模式允许读写并发busy_timeout让sqlite3在锁冲突时等待最多5秒而不是立刻报错。在AutoJs里调用execSql的时候可以把这两条PRAGMA合成一条SQL先执行一次或者在每个连接前都执行一遍实测下来能消灭大部分锁冲突问题。4.4 no such table查到的库是错的这个问题往往不是真的没建表而是操作了错误的数据库文件。最常见的场景是AutoJs写的数据库路径是/sdcard/data.db但你在adb shell里查的是/data/data/包名/databases/data.db两个文件根本不是同一个。排查时先执行.tables看看当前库里到底有哪些表再比对文件的修改时间和大小确认指向的是同一个文件。另外SQLite的表名是大小写敏感的Users和users是两个表写完建表语句之后建议用.schema验证一下。4.5 单引号导致SQL执行失败这是shell拼接SQL语句时最典型的坑。SQL语句里的字符串值要用单引号但如果值本身含有单引号比如英文缩写Its直接拼进去就会破坏整条SQL。SQLite的转义规则是把一个单引号变成两个单引号INSERT INTO users(name) VALUES(Its);在AutoJs源码里最安全的做法就是我在batchInsert里写的replace(//g, )。这里提醒一下SQL的字符串值只有通过单引号转义这一种办法不要在值里试图用反斜杠转义SQLite不认。4.6 大量数据写入很慢的优化心得逐条INSERT慢到怀疑人生这是新手最容易碰到的性能瓶颈。关键在于事务和同步模式。BEGIN; -- 这里放几十上百条INSERT COMMIT;加上事务之后写入速度通常会有几十倍的提升。如果再追求极限性能可以临时关闭同步刷盘和日志PRAGMA synchronous OFF; PRAGMA journal_mode MEMORY;这两个参数是牺牲掉一定的数据安全来换速度。如果服务器或脚本运行中突然断电数据库文件有损坏的风险。所以我只在导入临时数据时才这么干正式数据不会一直开这两个参数。4.7 中文内容显示成乱码sqlite3终端输出的编码跟当前shell的locale有关。AutoJs的execCommand拿到的返回值基本是UTF-8编码没太大问题。如果你在电脑端的adb shell里执行发现中文乱码多半是Windows控制台的代码页问题在adb shell前先执行chcp 65001切到UTF-8即可。在AutoJs这边如果发现乱码检查一下源文件本身是不是UTF-8保存的以及数据库写入时是否用了正确的编码。5. 一些更进阶的玩法5.1 用临时文件批处理SQL当你手里有一批SQL语句需要顺序执行时一条条去拼shell命令既笨拙又容易出错。更优雅的做法是先把SQL写成临时文件再让sqlite3一次性执行let sql BEGIN; INSERT INTO users(name) VALUES(张三); INSERT INTO users(name) VALUES(李四); COMMIT;; files.write(/sdcard/tmp_batch.sql, sql); let res runShell(sqlite3 ${dbPath} /sdcard/tmp_batch.sql); files.remove(/sdcard/tmp_batch.sql);利用shell的输入重定向sqlite3会逐行执行文件里的所有SQL。这在批量导入、加字段、重建索引这类操作上非常顺手而且中途出错时错误信息会精确到第几行排查效率极高。5.2 导出CSV给其他程序用AutoJs采集的数据经常要导出给电脑端做分析。用点命令可以直接把查询结果导出成CSVsqlite3 -header -csv /sdcard/test.db SELECT * FROM users; /sdcard/users.csv在AutoJs源码里可以动态拼这条命令来生成导出文件。注意-csv参数跟-header的顺序-header必须写在-csv之后这样列名才会包含在导出结果中。生成的CSV文件可以直接用Excel或者Navicat打开是中转数据的标准姿势。每个人的设备环境和自动化场景都不太一样但shell sqlite3这套组合的核心逻辑是通用的让AutoJs专注于业务逻辑和人机交互让sqlite3专注于数据存取用命令行做中间桥接。我实测下来同样的数据量这套方案比直接用AutoJs内置API要快出不少而且调试时能直接在电脑上拉数据看结果省掉了大把抓日志的时间。最后再分享一个小习惯所有涉及数据库路径的地方我都统一用一个变量管理避免多个脚本里写死不同的路径改起来真要命。本文还有配套的精品资源点击获取