开头
熟悉Auto.js的朋友应该都有过这样的体会:脚本里辛辛苦苦跑出来的数据,或者用户一次次填写的配置项,程序一结束就被清空了,下次运行一切归零。今天要聊的storages本地存储模块,就是为了解决这个痛点存在的。它是Auto.js自带的持久化方案,底层借助Android系统原生的SharedPreferences机制,把数据以键值对的形式固化成文件,随存随取,脚本重启、手机重启都不会丢。无论你是刚接触Auto.js的新手,还是想把脚本做得更人性化、更"有记忆"的进阶玩家,storages都是一块绕不开的基石。配置记住用户偏好、统计脚本运行次数、跨脚本传递参数,它都能胜任。这篇文章我会把storages的底层原理、全部核心方法、实战套路和踩坑经验一次讲透,给出一套可以直接照抄的写法。
1. storages到底是什么,为什么绕不开它
1.1 变量是"一次性"的,存储才是"永久"的
很多刚接触Auto.js的朋友容易陷进一个误区:认为脚本运行结束之后,之前定义的那些全局变量还会以某种方式保存着。实际上完全不是这么回事。脚本本质是一个JavaScript运行环境,进程一旦结束,内存里的所有数据都会被系统回收,变量、数组、对象统统消失。这是JavaScript这门语言的运行机制决定的,Auto.js也不例外。
那问题来了:我们大量实际场景需要"跨时间"的数据。举个例子,我做过一个定时打卡脚本,里面有一个参数是"打卡前等待秒数",用户第一次运行脚本的时候手动设置过一次,之后每次运行如果再让用户重新设置一遍,体验就很糟糕。又比如一个自动签到脚本,我需要记录"今天是否已经签过",避免重复执行,这个状态必须保存下来,否则脚本重启就判断不了。这些需求都指向同一个东西——持久化存储。
1.2 storages底层其实就是SharedPreferences
Auto.js的storages模块,本质上是对Android系统SharedPreferences的封装。SharedPreferences是Android里非常经典的一个轻量级数据存储方案,内部实现是一个XML文件,放在应用自己的私有目录下。对Auto.js而言,具体路径通常是/data/data/<包名>/shared_prefs/。感兴趣的朋友可以在脚本里调用files.cwd()或者用RE管理器去看一下,你会发现storages.create("myConfig")之后,目录下会多出一个myConfig.xml这样的文件,里面就是形如<string name="key">value</string>的键值对记录。
为什么说是"轻量级"?因为整个读写过程就是解析和生成XML,不需要数据库引擎,没有连接管理,也没有SQL语句,单次读写的开销非常小。你可以把它理解成一个随开随用的小抽屉,适合存放零散的配置项、状态标记、小规模数据。如果你需要存几百上千条结构化记录,那确实要考虑别的方案,但绝大多数Auto.js脚本的存储需求,storages都足够用了。
1.3 为什么推荐storages而不是普通文件读写
有些朋友习惯用files.write()直接写一个txt文件来保存数据。这种做法并非不行,我自己早期也这么干过,但用久了你会发现它有几个麻烦的地方。
首先,纯文本文件没有类型的概念。你写进去一个数字,读出来想当数字用,还得自己去parseInt、parseFloat;写进去一个数组,读出来还得自己按分隔符拆,一旦数据里本身包含分隔符就乱了套。其次,你得自己操心文件路径、编码格式、内容覆盖还是追加,出错的概率不低。storages把这些全部封装好了,你只需要调用put()和get()两个方法,存进去的对象类型和取出来的对象类型完全一致,不需要任何额外转换。这是它最核心的竞争力。
2. storages五大核心方法逐个拆解
2.1 storages.create:建立你的专属存储空间
使用storages的第一步,永远是调用storages.create(name)方法。这个name参数可以理解为存储空间的名称,也就是一个命名空间。不同名称的存储空间彼此独立,互不干扰。比如我可以建一个叫config的存储空间专门放用户配置,再建一个叫history的存放历史记录,两者井水不犯河水。
// 创建/获取名为config的存储空间 var storage = storages.create("config"); // 注意:如果config已经存在,这里获取的是同一个对象这里有一个很多新手会忽略的点:create方法不管存储空间是否已经存在,都会返回一个可以操作该空间的Storage对象。你不需要自己判断"这个空间是不是第一次创建"——第一次创建会新建XML文件,后面再调用就只是打开已有文件。在Auto.js高版本中,推荐使用storages.create,旧版API里的storages.get已经逐步不推荐了,虽然两者在很多版本里行为一致,但保险起见统一用create。
2.2 storage.put:把数据写进存储
put(key, value)是最常用的写入方法,它接收两个参数,key是键名,value是要保存的值。这个value支持的类型相当丰富,数字、字符串、布尔值、数组、对象都可以直接存。
var storage = storages.create("myData"); // 保存基本类型 storage.put("count", 100); storage.put("name", "张三"); storage.put("isFirstRun", false); // 保存数组和对象 storage.put("scores", [88, 92, 99]); storage.put("userInfo", { name: "李四", age: 25, tags: ["vip", "自动"] });看到这里可能有朋友会想:对象也能直接存?不会报错吗?答案是不仅不会报错,取出来的时候还能还原成完整对象。这是因为Auto.js在底层会自动把对象和数组通过JSON序列化之后再写入XML。这也意味着,如果你的对象里有函数、Date对象这样的特殊结构,序列化之后可能丢失或者变形,这是JSON天生的限制,下文会详细说。
另外需要留意:同一个key重复写入,后写的会覆盖先写的。这个覆盖是全量覆盖,不存在"合并"逻辑。比如你先存了{a: 1, b: 2},后面又存了{a: 9},取出来是{a: 9},b直接没了。想要部分更新,得先读出来再改,再写回。
2.3 storage.get:把数据从存储里取出来
写入的最终目的当然是读出来用。get(key, defaultValue)方法接收两个参数,第一个是键名,第二个是默认值。如果指定的key不存在,就返回默认值,这一点对防御式编程非常有用。
var storage = storages.create("myData"); // 读取已存在的key var count = storage.get("count", 0); var name = storage.get("name", ""); // 读取不存在的key,会得到默认值 var notExist = storage.get("notExist", "默认"); toast("不存在时的返回值:" + notExist);特别注意defaultValue的类型选择。因为存储本身是带类型的,所以取默认值时最好跟目标类型保持一致。如果你期望取一个数字,默认值就写0;期望字符串就写"";期望布尔就写false。千万不要写null或者不写默认值,否则后面运算很容易出现"undefined参与计算"这种让人抓狂的隐性Bug。
还有一个实际经验:用get取出对象之后,你拿到的是存储内的一个副本,不是引用。意思是说,你在代码里修改这个取出来的对象,并不会自动写回存储,必须再调用一次put才行。这个反直觉的特性坑过不少人。
2.4 storage.contains、remove、clear:查询与删除
这三个方法的作用很明确,但使用频率却不低。
var storage = storages.create("myData"); // 判断某个key是否存在 if (storage.contains("userInfo")) { toast("userInfo已存在"); } else { toast("userInfo不存在,首次运行?"); } // 删除单个key storage.remove("tempKey"); // 清空整个存储空间 storage.clear();contains常用于做"首次运行"判断。比如脚本第一次启动时弹出引导说明,之后不再弹出,就用contains检测一个标记key是否存在。remove用于清理临时数据,注意它只删除指定key,其他数据不受影响。clear就比较暴力了,会把当前命名空间下所有键值对全部清空,使用前务必三思,这种操作没有后悔药,删完就真的没了。
2.5 跨脚本共享:同一个name实现数据互通
storages一个非常实用的特性是跨脚本共享。Auto.js的多个脚本使用同一个storages.create("xxx")名称,操作的就是同一个存储空间。我经常利用这个特性做一个"配置写入脚本"和一个"业务执行脚本"的分离。
// 脚本A:写入配置 var store = storages.create("sharedConfig"); store.put("targetApp", "抖音"); store.put("likeCount", 50); // 脚本B:读取配置 var store = storages.create("sharedConfig"); var targetApp = store.get("targetApp", ""); var likeCount = store.get("likeCount", 50); toast("脚本B读到了:" + targetApp + "," + likeCount);这意味着你可以把一些公共参数集中在一个配置文件脚本里维护,其他脚本只管读取,维护起来会方便很多。不过要注意,所谓"共享"仍然是建立在同一台手机的基础上的,换了一台设备,数据并不会跟着走。
3. 完整实战:做一个带状态记忆的自动签到脚本
3.1 需求梳理与整体设计
纸上谈兵讲了一堆API,不如直接上一个完整的实战案例。这里我设计一个带状态记忆的自动签到脚本,它需要做三件事:一,记录用户配置的签到账号和签到时间;二,记录今天是否已经签到,避免重复执行;三,统计累计签到天数。
整体设计上,我建两个存储空间:一个叫userConfig,存放账号、签到偏好这些长期配置;另一个叫signHistory,存放每日签到状态和累计天数。分开建的好处是职责清晰,清理历史记录时不会误删用户配置。
// 初始化存储 var configStorage = storages.create("userConfig"); var historyStorage = storages.create("signHistory");3.2 配置写入与读取
用户的配置需要在首次运行时引导用户填写,之后每次运行直接读取,不需要重复询问。
// 首次运行判断 if (!configStorage.contains("configured")) { // dialogs模块弹出输入框,这里简化处理 var account = dialogs.rawInput("请输入签到账号", "user@example.com"); var time = dialogs.rawInput("请输入每日签到时间(格式 HH:mm)", "08:30"); configStorage.put("account", account); configStorage.put("time", time); configStorage.put("configured", true); toast("配置已保存,下次将自动读取"); } else { var account = configStorage.get("account", ""); var time = configStorage.get("time", ""); toast("读取到配置:" + account + ",每天" + time + "签到"); }这段代码里有一个值得注意的细节:我用了configStorage.put("configured", true)这个布尔值来做首次运行标记。布尔值比判断字符串简单得多,而且contains配合布尔值使用语义非常清晰。
3.3 日期状态判断与更新
每日签到的"是否已签"逻辑,关键在记录当天日期。我可以用new Date().toDateString()生成如"Mon Mar 17 2025"这样的日期字符串,然后和存储里记录的日期比较。
// 获取今天的日期字符串 function todayString() { var d = new Date(); var year = d.getFullYear(); var month = d.getMonth() + 1; var day = d.getDate(); return year + "-" + month + "-" + day; } var today = todayString(); var lastSignDate = historyStorage.get("lastSignDate", ""); if (lastSignDate === today) { toast("今天已经签到过了,不需要重复执行"); exit(); } // 开始执行签到逻辑... toast("正在执行签到..."); // 这里是实际的UI操作流程:打开App、点击签到按钮等 sleep(2000); // 签到成功,更新状态 historyStorage.put("lastSignDate", today); var totalDays = historyStorage.get("totalDays", 0); historyStorage.put("totalDays", totalDays + 1); toast("签到成功!累计签到" + (totalDays + 1) + "天");这套逻辑我已经在实际脚本里验证过,即使脚本执行到一半被杀掉,只要最后的put没执行,下次运行仍然会认为"今天还没签",这正好符合需求——签到动作没有完成,就不应该计入记录。
3.4 用真实数据演示完整流程
我把上面的代码整合成一个可以运行的完整脚本,去掉具体UI点击部分,保留了存储相关的全部逻辑。实际跑一次,日志输出如下:
- 第一次运行:
首次运行,请配置→ 输入账号和签到时间 →配置已保存,下次将自动读取→ 执行签到 →签到成功!累计签到1天 - 第二次运行:自动读取配置 →
今天已经签到过了,不需要重复执行→ 脚本退出 - 把系统日期改到明天再运行:读取配置 → 执行签到 →
签到成功!累计签到2天
整套状态流转非常清楚。这就是storages带给脚本的"记忆能力"。
4. 数据类型的底层逻辑与进阶用法
4.1 序列化机制:为什么对象存取这么方便
前面提到storages底层会自动把对象、数组序列化成JSON再写入XML。这句话展开讲其实就是:你在put一个对象时,Auto.js内部先调用了类似JSON.stringify的机制,把它变成纯文本;get的时候再用JSON.parse把纯文本还原成对象。
这个机制带来两个直接的好处。一是存取方便,不用手动转换;二是跨版本兼容,因为JSON是纯文本格式,哪怕Auto.js版本升级了,只要底层解析没有大改,数据依然能读出来。但也有局限性,上面提到的函数会丢失、Date会变成字符串、循环引用的对象会直接报错。实用建议是:storages里只放"纯数据"——普通对象、数组、字符串、数字、布尔,别放函数和复杂实例。
4.2 用namespace隔离管理多组数据
实际开发中一个脚本往往需要同时管理多组数据。你可以全部塞进一个大对象,也可以拆分成多个命名空间。我的建议是后者,理由有三。
第一,隔离性好。用户配置和临时缓存混在一起,如果要对临时缓存执行clear(),用户配置也会被误删。第二,可读性好。看到userConfig.get("account")就知道这是用户配置,看到cacheStore.get("listData")就知道是临时数据,代码语义非常清晰。第三,降低键名冲突概率。两个逻辑域都叫name,如果放在同一个空间里就会互相覆盖,分开则各用各的。
// 推荐:按业务域拆分 var userCfg = storages.create("module_user"); var taskCfg = storages.create("module_task"); var cacheCfg = storages.create("temp_cache"); // 不推荐:全部塞一个空间 var allInOne = storages.create("everything");4.3 初始化与迁移策略
有一种场景很容易被忽视:storages里的数据结构随着脚本迭代是会变化的。比如第一个版本存的是{ name: "xxx" },第二个版本变成了{ fullName: "xxx", nickName: "yyy" }。老版本的用户升级后,读出来的数据可能缺少新字段,导致界面上显示异常。
我的应对策略是引入"存储版本号"机制。在存储空间里放一个storageVersion字段,每次脚本启动时检查版本号,如果发现版本偏低,就执行一次迁移逻辑,把老数据转换成新结构,再把版本号更新。这就好比给数据文件做了个升级补丁,非常实用。
var STORAGE_VERSION = 2; var store = storages.create("appData"); if (store.get("storageVersion", 1) < 2) { // 迁移逻辑:把老字段转为新字段 var oldName = store.get("name", ""); store.put("fullName", oldName); store.put("nickName", oldName); store.remove("name"); store.put("storageVersion", 2); }4.4 加密存储的替代方案
storages存的数据是明文XML,敏感信息(比如账号密码)直接存会有隐私风险。如果手机被root,数据文件可以被直接读取,这个一定要心里有数。我的建议是:千万别用storages存真实密码,要存就存加密后的密文。
具体做法是先用crypto模块的摘要或加解密方法处理一下,再写入存储。比如用MD5存校验信息,用AES加密存关键凭据。下面是一个简单的数据脱敏示例:
var store = storages.create("secured"); // 加密后存储 var rawPassword = "abc12345"; var encrypted = crypto.encrypt(rawPassword, "aes-256-cbc", "myKey"); store.put("password", encrypted); // 注意:仅示意,正式使用需妥善管理密钥 // 解密读取 var savedEncrypted = store.get("password", ""); var decrypted = crypto.decrypt(savedEncrypted, "aes-256-cbc", "myKey");这条经验是我自己踩过坑才总结出来的。早期我写脚本习惯把账号密码直接明文存storages,后来一个朋友告诉我他手机里的脚本数据被别的App扫描到了,虽然不至于造成多大损失,但想想还是后怕。从那时候起,凡是涉及敏感信息,我一律加密后再入存储。
5. 常见问题与排查技巧实录
5.1 问题一:get取出来是undefined
这是新手最高频的问题。原因通常有两种:一是key写的和put时不一致,比如大小写不同或者多了个空格;二是压根没有put过这个key,而get的时候又没有传默认值。解决办法其实很简单,养成两个习惯:第一,key的命名统一用小驼峰,比如lastSignDate,并且全项目保持同一套命名风格;第二,get的时候永远带上默认值,哪怕就是get("xxx", ""),也不要让返回结果有无穷的可能性。
特别提醒:storage.get("key", null)取出来的null,在参与字符串拼接时会出现"null"这种字面量,参与条件判断时又是false,非常容易让代码产生诡异行为。我建议把所有"可能不存在"的key都用类型匹配的默认值兜底。
5.2 问题二:存进去对象,取出来变了
这个问题的根源在于JSON序列化的机制。比如下面这段代码:
var store = storages.create("test"); var d = new Date(); store.put("dateObj", d); var out = store.get("dateObj", ""); log(out); // 输出如 2025-04-07T08:30:00.000Z 这种字符串,而不是Date对象Date对象在JSON序列化时会被转换成ISO字符串,取出来自然就不再是Date对象了。函数也是同样道理,直接丢失。解决方法是只存可序列化的纯数据,如果要存时间戳,就存d.getTime()这种数字形式;如果要存日期字符串,就自己格式化好后存入。
5.3 问题三:跨脚本读取不到数据
有朋友反馈说脚本A里put了数据,脚本B里却get不到。排查这个问题的第一步,先确定两个脚本使用的存储空间名称是否完全一致,一个字符都不能差,包括大小写和空格。第二步,确认两个脚本是否真正操作了同一个Auto.js环境,如果用的是不同版本的Auto.js,数据目录可能不同。第三步,用files.exists或者文件管理器直接看那个XML文件在不在,里面有没有写入记录。
实际工作中我还遇到过一种情况:脚本A写入了数据,但脚本A自己异常崩溃了,崩溃前没有flush,数据停留在内存缓存里,脚本B读的时候自然是空的。SharedPreferences的写操作通常有异步缓存机制,强制杀进程可能导致数据丢失。解决办法是写入后用storage对象内部提供的flush相关能力,不过Auto.js封装层不一定会暴露这个接口,所以更稳妥的做法是:重要数据写入后,可以简单sleep()几百毫秒,给系统一个落盘的时间,然后再进行其他操作。
5.4 问题四:多设备同步怎么处理
storages只能存在本机,没有同步能力。如果你的使用场景涉及多台设备,比如家里一台手机、公司一台手机都在跑同一个脚本,那数据就是各存各的,互不相通。我见过有人为了让两台设备"同步"配置,直接把XML文件导出再导入,这个是可行的,但非常笨重。更合理的方案是把需要共享的数据放在云端,比如HTTP接口、FTP服务器,或者云笔记,Auto.js本身提供http模块可以轻松实现这个能力。本地存储负责本地状态,云端负责跨设备同步,两者结合才算完整方案。
5.5 性能避坑:不要在循环里频繁读写
put和get虽然轻量,但底层毕竟是XML解析和文件IO,频繁在循环里调用会导致性能下降。我见过一个朋友在for (var i = 0; i < 1000; i++)的循环里执行put保存每一条数据,结果脚本运行时间从几秒飙升到几十秒。
正确的做法是:循环内只操作JavaScript对象数组,循环结束后一次性put整个数组。数据量小、操作不频繁的时候,storages是利器;一旦遇到高频读写,就要把整个数据在内存中维护,只在合适的时机批量落盘。
// 错误的做法 var store = storages.create("bad"); for (var i = 0; i < 500; i++) { store.put("item" + i, i); } // 正确的做法 var store = storages.create("good"); var list = []; for (var i = 0; i < 500; i++) { list.push(i); } store.put("items", list);5.6 排查工具与调试技巧
最后分享几个我常用的调试技巧。第一,用log输出读取到的完整内容,log(storage.get("key", "")),确认存储内容是否符合预期。第二,用Auto.js自带的控制台查看运行日志,注意区分toast和log,调试阶段用log比toast可靠,因为toast会一闪而过。第三,如果条件允许,直接查看XML文件内容,这是最彻底、最不会骗人的排查方式。
我曾经写过一个诊断函数,专门用来列出某个存储空间下的所有键值,调试时非常有用:
function dumpStorage(name) { var store = storages.create(name); var keys = store.keys ? store.keys() : null; // 部分版本支持keys() if (keys) { log("存储空间[" + name + "]包含" + keys.length + "个键:"); for (var i = 0; i < keys.length; i++) { log(keys[i] + " => " + store.get(keys[i])); } } else { // 版本不支持keys()时,只能逐个判断常见key log("当前Auto.js版本不支持遍历keys,请手动检查业务key"); } } dumpStorage("userConfig");6. 把它用到更多场景中:storages的边界与延伸
6.1 适合storages的场景清单
根据我自己的使用经历,适合用storages的场景可以列一个清单:用户配置项(账号、时间、开关状态)、脚本运行状态(今日是否已执行、上次执行时间)、累计统计(运行次数、成功次数、积分余额)、临时数据交换(脚本A产出的结果供脚本B读取)、功能开关(远程开关的本地缓存值)。这些场景的共同特点是:单条数据量小、读写频率低、数据结构简单。符合这三个特点,用storages就是最佳选择。
6.2 不适合storages的场景
如果你的数据量动辄几千条,每条又是复杂的多字段记录,比如要保存一个完整的通讯录、一批商品列表,那storages就不是好选择了。这种需求应该考虑数据库方案,比如Auto.js可以配合SQLiteDatabase使用,或者自己维护一个JSON文件。另外,高并发高频写的场景也不适合,前面性能部分已经说过。还有一个很容易忽视的点:不要用storages存大型图片或文件的二进制内容。XML是文本格式,存base64编码的二进制数据会导致文件体积迅速膨胀,读写都会变得非常慢,这种情况直接用files模块存文件才是正道。
6.3 与Auto.js其他API配合的思路
storages的威力往往体现在和其他模块组合使用的时候。比如和events模块配合,实现"跨脚本事件携带存储数据";和http模块配合,实现"远程配置本地缓存";和dialogs模块配合,实现"配置向导持久化"。这些组合里,storages扮演的角色都是那个"记忆核心"。
我以前做过一个"智能提醒"脚本,就是storages + notifications的组合:storages记录上一次提醒时间,到点之后脚本判断是否应该再次提醒,使用通知弹窗。整个状态管理全是storages在支撑,代码量小、逻辑清晰、运行稳定。这种组合思路才是把这个基础模块用活的方式。
6.4 版本兼容性提示
Auto.js不同版本的storages行为略有差异。在较老的4.x版本里,创建存储空间的方法叫storages.create(),也有用storages.get()的地方。到了更新的Pro版本和各类二次开发版本,API基本保持兼容,但有个别版本在put对象时序列化策略不同。最稳妥的策略是:写代码之前先在你自己的Auto.js环境里跑一次小测试,确认当前版本的storages对对象存储的表现是符合预期的。这个测试成本很低,却能避免后续大量返工。
7. 写在最后的实操心得
讲了这么多,最后沉淀几条我个人最深的体会。第一,storages是Auto.js里最被低估的基础模块,它不只是"存数据用的",更是整个脚本"智能化"的根基。没有存储能力的脚本,永远只能做一次性任务,有了存储能力,脚本才能积累经验、记住偏好、判断状态。第二,命名规范怎么强调都不过分。你的存储空间名称、key名称,直接决定了项目维护的舒服程度。我见过无数人的代码里出现store.put("a", 1)这种写法,一周之后再去看,没人记得a是什么。第三,重要数据一定要考虑加密和备份。storages方便归方便,但它就是明文XML,敏感数据裸奔在系统目录里,这个风险必须自己心里有数。
最后再分享一个小技巧:我习惯在每个脚本顶部放一段"存储自检"代码,启动时快速检查核心key是否存在,不存在就重建默认值。这个小动作能避免大量因为存储缺失导致的运行异常,成本极低,收益很实在。
// 存储自检:确保关键数据存在 function ensureStorageReady() { var store = storages.create("appData"); if (!store.contains("created")) { store.put("created", new Date().toLocaleString()); store.put("runCount", 0); store.put("configVersion", 1); } var count = store.get("runCount", 0); store.put("runCount", count + 1); } ensureStorageReady();storages这个模块不难,但越基础的能力越值得花时间吃透。把上面这些方法、场景、避坑点消化掉,你的Auto.js脚本会从"写完跑完就忘"变成"越用越懂你",这个体验上的提升是立竿见影的。