
一、我们想解决什么问题
咱们先聊个真实的场景。
你打开一个阅读类 App,晚上躺在床上看,屏幕太亮眼睛不舒服,于是你切到了"深色模式"。界面变成黑底白字,舒服了。你翻了十来页,标记了一个书签,然后关掉 App 睡觉。
第二天你打开同一个 App------好家伙,深色模式没了,又变成白晃晃的亮色主题;上次看到的小说第 52 章第 7 页也不记得了,又得从头翻。你是不是想骂人?
这是不是特别眼熟?
类似的场景太多了:
- 你给记事本 App 调了个字体大小,下次打开又变回默认字号
- 你设置了"开启时自动刷新",关掉 App 再进,这个选项又取消了
- 你输入了一个收货地址格式的偏好,下次打开表单自动填充的信息全不见了
- 你就想让某个开关保持"开"的状态,但 App 每次打开都跟你玩"失忆"
这些问题的本质是什么?数据没有本地存下来。
说得技术一点,叫做"没有做数据持久化"。持久化这个词听起来唬人,说白了就是:你把一个值写进去了,下次 App 打开的时候它还在那儿------就像你在冰箱里放了一瓶可乐,第二天打开冰箱它还在,没消失。
HarmonyOS 给我们提供了好几种本地存储方案。但如果你要存的不是什么大文件、不是什么复杂的数据库,只是一些"小东西"------比如用户偏好设置、开关状态、最近一次的操作记录------那用 Preferences 是最合适的。
打个比方:如果你要搬家,大件家具叫搬家公司(那是数据库或者文件存储干的活儿),而 Preferences 就是你自己随身带的那个小背包,装钥匙、钱包、手机------轻便、快速、随拿随取。
Preferences 是什么?
它的全名叫"轻量级偏好存储"。你从名字就能看出来------它专门管"偏好"这类东西。在 HarmonyOS 里,它是以 KV 对(Key-Value,键值对)的形式来存数据的。
什么叫 KV 对?想象你有一个储物柜。每个柜子上面贴了个标签(Key),比如"深色模式开关",柜子里面放了对应的东西(Value),比如"true"(表示开启了深色模式)。你要取东西的时候,只要看标签就行了,不用把整个柜子翻一遍。
就是这么简单。
所以今天我们聊的,就是怎么用 Preferences 来搞定这些"小数据"的本地存储问题。你不用操心什么数据库建表、什么 SQL 语句、什么文件 IO 路径------Preferences 把这些都封装好了,你只需要告诉它:"帮我存一下这个值"和"帮我取一下这个值"。
二、数据模型设计
在开始写代码之前,咱们得先想清楚一件事:我们要存哪些数据?
这就好比你去超市买东西之前,最好列个清单------不然进去就蒙了,出来发现想买的没买,不需要的买了一堆。
今天我们要做什么示例?
咱们来做个"阅读设置面板"的 Demo。这个面板包含以下几种设置项:
- 深色模式开关 ------ 用户是否启用了深色主题。这个值只有两种状态:开或关。用布尔值(boolean)最合适。
- 字体大小 ------ 用户选择的字号。可能是"小""中""大""超大"这几个选项。用字符串(string)存最方便。
- 上次阅读位置 ------ 用户上次看到哪一页。比如第 128 页。这是一个数字,用 number 类型。
- 用户昵称 ------ 用户在 App 里设置的自定义名称。这个是个字符串。
四个设置项,四种不同的数据类型,刚好能覆盖日常使用的绝大多数场景。
用 TypeScript 定义数据模型
在 HarmonyOS 开发里,我们通常用 TypeScript 的 interface 来定义数据结构。这就好比搭了个"模板",后面的数据都得按这个模板来。
来看代码:
typescript
// 阅读设置的数据模型
interface ReadingSettings {
// 深色模式开关
darkMode: boolean;
// 字体大小:small / medium / large / xlarge
fontSize: string;
// 上次阅读的页码
lastPage: number;
// 用户昵称
userName: string;
}
这个 interface 定义出来之后,它就像一张表格的"表头"------规定了每一列叫什么名字、是什么类型。我们的 App 在存数据、取数据的时候,都会按照这个格式来对齐。
你可能会问:如果后面还要加别的设置项怎么办?比如加个"行间距"或者"翻页动画"?很简单,在 interface 里加一行字段定义就行了。Preferences 的核心思想就是------你想存什么就存什么,只要给每个数据起个名字(Key),它就能帮你管好。 没有数据库那种"我要先改表结构"的麻烦。
这就是 KV 存储的灵活之处:每一对 key-value 都是独立的,互不干扰。你可以先存一个键,过两天再加一个新的键,完全不用动之前的数据。
但这里也得留个心眼:虽然灵活,但别滥用。Preferences 设计来存的是"配置项""偏好设置"这类少量数据,不是说让你把整个应用的业务数据全塞进去。比如用户的一千条聊天记录就别放这里了------那得用数据库。Preferences 的定位就是那个"随身小背包",不是"搬家货车"。
三、核心设计决策
好,现在我们已经知道要存什么了。那接下来面临一个选择:HarmonyOS 提供了好几种本地存储方案,到底用哪个?
我把常见的三种方案拉出来对比一下,这样你一眼就能看出差别。
| 对比维度 | Preferences(轻量存储) | KVStore(分布式键值) | 关系型数据库(RDB) |
|---|---|---|---|
| 数据量级 | 小(通常几百条以内) | 中等 | 大(万条以上) |
| 使用难度 | ⭐ 极简,两三个 API | ⭐⭐ 需要初始化 | ⭐⭐⭐⭐ 需要建表、写 SQL |
| 是否分布式 | ❌ 仅本地 | ✅ 可跨设备同步 | ❌ 仅本地 |
| 适合场景 | 用户偏好、开关状态、配置项 | 需要多端同步的数据 | 结构化复杂业务数据 |
| 数据类型 | String / Number / Boolean / Array | String / Number / Boolean | 强类型,支持各种 SQL 类型 |
| 读取性能 | 极快(内存缓存) | 快(但有同步开销) | 中等(SQL 查询开销) |
现在说说我的选择。
我选 Preferences,理由很简单------我们的需求恰好就落在它最擅长的区域里:
第一,数据量很小。 深色模式开没开、字体多大、上次看到第几页......这些东西加起来可能十个手指头就数得过来。你为了存这不到十个值,去建一个关系型数据库的表?那就像用搬家货车运一个背包的东西------不是不行,但有点夸张了。
第二,不需要跨设备同步。 咱们这个阅读设置的偏好,就只在当前的手机上生效,不需要说"我在手机上设了深色模式,平板也跟着变"。KVStore 的分布式能力确实很厉害,但这里有句实在话:能力越强,需要操的心也越多。如果你的 App 就只在单设备上跑,没必要给自己加活。Preferences 一把梭就够了。
第三,开发效率。 Preferences 的 API 设计得非常"直觉化"------存数据是 put(),取数据是 get(),删数据是 delete()。就这么三个核心操作,简单到你基本不用翻文档。相比之下,关系型数据库你得先建表、再写 CRUD 的 SQL、还得处理事务......杀鸡不用牛刀,对吧?
第四,性能。 Preferences 在读取数据的时候会把常用数据缓存在内存里,第二次读的时候基本是毫秒级的,甚至感觉不到延迟。用户每次打开 App,要加载设置项的时候,Preferences 几乎是秒回的体验。这叫"润物细无声"------用户感觉不到它在工作,但每次体验都很丝滑。
但这里我也得说句公道话:Preferences 也不是万能的。
如果你的数据量大了------比如超过几百条,或者单条数据特别大(超过了 10KB),Preferences 的性能就会开始打折。这时候就该考虑数据库了。还有,Preferences 只支持基础的 KV 存储,不支持复杂查询。比如说你想"查所有字体设为'大'的用户",那 Preferences 搞不定------它没办法像数据库那样 WHERE 一把梭。
所以选型这件事,没有什么"最好的方案",只有"最合适的方案"。你的需求决定了你的选择。我们今天要处理的"阅读设置"刚好在 Preferences 的舒适区里,那我们就用它。
四、完整代码实现
好了,理论聊完了,咱们来动手。我保证这部分的代码不会多------每个代码块只展示最核心的部分,加起来不超过五个代码块,每个不超过五十行。
先把完整的工作流程走一遍:创建 Preferences → 存入数据 → 读取数据 → 删除数据。就这么四个步骤。
4.1 创建 Preferences 实例
首先,我们要创建一个 Preferences 的实例。这个过程叫做"获取 Preferences 对象"------通俗说就是"先打开那个存数据的小本本"。
typescript
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
// 获取上下文
const context: common.Context = getContext(this);
// 创建一个名为 'reading_prefs' 的 Preferences 存储实例
const prefs: preferences.Preferences = await preferences.getPreferences(context, 'reading_prefs');
这段代码是什么意思呢?
第一行,import { preferences } from '@kit.ArkData'------这是在导入 HarmonyOS 提供的数据管理 Kit,Preferences 就住在里面。
第三行,getContext(this)------拿到当前应用的上下文。你可以把上下文理解成"应用的身份证明",后面很多系统级的操作都需要用到它。Preferences 也是一样,它需要知道"这是哪个 App 的数据",所以得先拿上下文。
第四行是核心:preferences.getPreferences(context, 'reading_prefs')------这是在告诉系统:"帮我在本地创建一个名为 reading_prefs 的存储区域"。这个 'reading_prefs' 就像是一个文件夹的名字,每个 Preferences 实例都有自己的名字。后续的读写操作都基于这个区域。
值得一提的是,getPreferences 是异步的(async),所以前面加了 await。它会在第一次调用时创建文件,后续调用就直接读缓存------所以第二次及以后的速度非常快。
4.2 写入数据
实例拿到了,接下来就是存数据。Preferences 的存数据操作叫 put------英文里就是"放进去"的意思,非常形象。
typescript
// 写入一条数据:是否使用深色模式
await prefs.put('darkMode', true);
// 写入字体大小
await prefs.put('fontSize', 'medium');
// 写入上次阅读位置(页码)
await prefs.put('lastPage', 128);
// 写入用户昵称
await prefs.put('userName', '小张三');
// 别忘了 flush!把数据真正刷到磁盘上
await prefs.flush();
看到没?就这么简单。put 的第一个参数是"标签"(Key),第二个参数是"值"(Value)。你把标签贴好了,东西放进去了,齐活。
你可能会注意到,我写了好几个 put,最后才调了一个 flush。这是 Preferences 的一个设计特点:写操作默认是先缓存在内存里的 ,就像你把东西先放在了桌面上,还没有收到抽屉里。调了 flush() 之后,内存里的数据才会真正写到磁盘上,确保 App 关了也不会丢。
为什么不每次 put 完都自动 flush?因为磁盘读写比内存慢得多。如果用户连续修改十个偏好设置,每次都刷到磁盘,那就多浪费了九次 IO 操作。Preferences 的做法是让开发者自己决定什么时候一次性刷下去------这是一个性能取舍。
当然,如果你忘记了调 flush,而 App 突然崩溃了,那未刷盘的数据就会丢失。所以记得:改完设置了,就调一次 flush。
从 API 12 开始,Preferences 也支持了不带 flush 的"自动刷盘"模式(通过 put 时传入特定参数可以自动持久化),但为了更安全的习惯,手动 flush 仍然是我推荐的写法------你能掌控数据什么时候落盘。
4.3 读取数据
存完了,怎么取出来?用 get。
typescript
// 读取深色模式开关(默认值设为 false)
const isDarkMode: boolean = await prefs.get('darkMode', false);
// 读取字体大小(默认值设为 'medium')
const fontSize: string = await prefs.get('fontSize', 'medium');
// 读取上次阅读页码(默认值设为 1)
const lastPage: number = await prefs.get('lastPage', 1);
// 读取用户昵称(默认值设为空字符串)
const userName: string = await prefs.get('userName', '');
console.info('深色模式:' + isDarkMode);
console.info('字体大小:' + fontSize);
console.info('上次看到第' + lastPage + '页');
console.info('用户昵称:' + userName);
get 也是两个参数:第一个是 Key(你要取什么),第二个是默认值。
这个默认值很关键。你想,如果用户是第一次打开 App,之前根本没存过任何数据,那 get('darkMode') 应该返回什么?null?undefined?还是 false?Preferences 的做法是让你提供一个"如果没存过就返回这个"的备用值。这就是第二个参数的作用。
这么做有一个好处:你的代码里不用写大量的空值判断。 默认值体系保证了返回值永远是有效数据------如果用户没设过,那就走默认的。对用户来说,App 打开就有合理的默认配置,体验很流畅。
这里有个小细节:get 方法的返回值类型需要和默认值的类型匹配。也就是说,prefs.get('darkMode', false) 的时候,默认值传了 false(boolean),那返回的就是 boolean 类型。如果你传 prefs.get('darkMode', 'false'),默认值变成了字符串,那返回类型就是 string。所以写的时候要注意类型一致性。
4.4 删除单条与清空
有时候用户想把某个设置恢复到默认状态------比如重置字体大小。这时候就需要删除数据了。
typescript
// 删除单条数据
await prefs.delete('lastPage');
await prefs.flush();
// 或者整个清空------就像恢复出厂设置
await prefs.clear();
await prefs.flush();
delete 是删一条;clear 是把整个 reading_prefs 里的数据全清空。用哪个取决于你的场景------是想"删掉一个设置",还是"把用户的所有偏好恢复默认"。
HarmonyOS 还有一个跟 Preferences 配合得很好的机制:持久化订阅。你可以监听某个 Key 的变化,比如当用户改了深色模式,你可以自动更新 UI:
typescript
prefs.on('change', (key: string) => {
if (key === 'darkMode') {
// 自动切换主题
applyTheme(prefs.get('darkMode', false));
}
});
但要注意,这个监听器是比较轻量的通知机制,不是事件总线------别在上面挂太多复杂逻辑。而且从 API 12 开始,建议用 PreferencesChangeCallback 配合 on('multiSettingsChange') 来批量处理变更,性能和可维护性更好。这里先提一句,不展开,后续深入聊监听机制的时候再说。
4.5 完整的使用函数
最后,我把上面这些操作整合成一个完整的使用示例,方便你直接复制过去跑:
typescript
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
export class SettingsManager {
private prefs: preferences.Preferences | null = null;
private readonly storeName = 'reading_prefs';
// 初始化 Preferences
async init(context: common.Context): Promise<void> {
this.prefs = await preferences.getPreferences(context, this.storeName);
console.info('Preferences 初始化完成');
}
// 保存所有阅读设置
async saveSettings(settings: ReadingSettings): Promise<void> {
if (!this.prefs) throw new Error('未初始化');
await this.prefs.put('darkMode', settings.darkMode);
await this.prefs.put('fontSize', settings.fontSize);
await this.prefs.put('lastPage', settings.lastPage);
await this.prefs.put('userName', settings.userName);
await this.prefs.flush();
console.info('设置已保存');
}
// 读取所有阅读设置
async loadSettings(): Promise<ReadingSettings> {
if (!this.prefs) throw new Error('未初始化');
return {
darkMode: await this.prefs.get('darkMode', false),
fontSize: await this.prefs.get('fontSize', 'medium'),
lastPage: await this.prefs.get('lastPage', 1),
userName: await this.prefs.get('userName', ''),
};
}
// 重置指定设置(删掉对应 Key)
async resetSetting(key: string): Promise<void> {
if (!this.prefs) throw new Error('未初始化');
await this.prefs.delete(key);
await this.prefs.flush();
}
}
这个 SettingsManager 你可以理解成一个"管家"------它帮你统一管理所有跟设置相关的存取操作。init 负责启动并连接数据源,saveSettings 负责批量保存,loadSettings 负责批量读取,resetSetting 负责删除某一条。麻雀虽小,五脏俱全。
你看,没有任何复杂的 SQL 语句,没有建表的过程,没有事务管理------就是 put、get、delete、flush 四个操作,来回组合。这就是 Preferences 的"轻量"所在。
五、深度技术原理
很多文章写到上面那一步就结束了------代码给你了,你用就完了。但既然是"揉碎了讲",咱们还得往深挖一层,让你不光会用,还知道它背后是怎么工作的。
Preferences 的内部结构
Preferences 在底层到底是怎么存数据的?它在文件系统里是一份 XML 文件。
没错,就是那种带着 <tag> 和 </tag> 的 XML 格式。每个 Key 对应一个 XML 节点,Value 编码在里面。不过这些都是系统帮你打理好的,你不需要自己去碰这个文件,也不需要关心它的格式------但你得知道有这么个东西存在,这样你才能理解它的一些行为。
比如,Preferences 不擅长存大量数据,为什么?因为这个 XML 文件每次 flush 的时候都是全量写入的------不是说"只修改了一个 key,就只改那一行",而是把整个文件重新写一遍。如果你的数据量大了(几百个 key 或者单条数据很大),磁盘 IO 开销就会线性增长。
再比如说,为什么 Preferences 的读取很快?因为它有个内存缓存层 。初始化的时候,它会把这个 XML 文件整个读到内存里,变成一个 Map(映射表)。后续的 get 操作根本不碰磁盘,直接从内存 Map 里取,所以速度特别快。
这个设计其实挺巧妙的:写入走"先记内存 → 后统一刷盘"的延迟写策略,读取走"一次加载 → 全程内存访问"的路子。读多写少?完美匹配。
与其他存储方案的数据对比
咱们来做一个非常接地气的性能对比。
假如你的 App 里有 50 条配置项,每条数据平均大小 256 字节,总数据量大约只有 12.5KB------还不到一张普通自拍照片的十分之一。这种量级下,Preferences 的初始化加载时间基本在 5ms 以内,单次 get 操作在 0.1ms 级别。如果你用数据库来存同样这 50 条数据,光是建立数据库连接、建表、写 SQL,启动时间就要多费几十甚至上百毫秒。为了 50 条配置,不值得。
但反过来,如果你的数据量到了 10 万条,Preferences 就会变得很吃力------因为每次 flush 都是全量写,10 万条数据序列化成 XML 再写磁盘,那延迟就不是毫秒级了,可能是几百毫秒甚至秒级了。而且全部加载到内存,也占空间。到那个量级,数据库才是正解。
所以记住这个判断尺度:50 条以内无脑 Preferences,500 条以上慎重,上千条换数据库。 中间带区域不绝对,得结合单条数据大小和读写频率判断。
线程安全与并发
Preferences 在 HarmonyOS 里设计为线程安全的。也就是说,你不用担心多个异步任务同时读写同一个 Preferences 实例会导致数据错乱。系统内部做了锁机制来保证一致性。
但这不代表你完全不用操心。一个常见的坑是:不要同时创建多个指向同一个存储名字的 Preferences 实例。 比如你在 A 页面调了 getPreferences(context, 'reading_prefs'),又在 B 页面调了同样名字的 getPreferences(context, 'reading_prefs')------虽然系统会尽量复用同一个实例,但在某些极端并发场景下,还是可能出现不一致的情况。最佳实践是:同一个存储名字,在整个应用生命周期里只初始化一次,全局共享。
上面代码里写的 SettingsManager 设计成了单例的使用模式------通过类来封装,外部只调用它的方法,不直接创建第二个 Preferences 实例。这样做的好处是:所有数据访问都通过同一个管家,不会出现"左手不知道右手做了什么"的问题。
Preferences 的生命周期
Preferences 的数据会跟随应用的生命周期。简单来说:
- 应用卸载 → Preferences 数据被清除
- 应用清除缓存 → Preferences 数据可能被清除
- 应用更新(覆盖安装) → Preferences 数据通常保留
- 系统设置中清除应用数据 → Preferences 数据被清除
这一点在设计时要留意。如果你的 App 存储了"用户是否已经完成新手引导"这个标记------当 App 更新后,Preferences 数据在大多数情况下是保留的,所以用户不会重新走一遍新手引导。但如果你需要"每次更新都重置某些设置",那你就得在更新后的首次启动时,手动去删除或重置那些 key。
HarmonyOS 从 API 10 开始提供了 preferences.removePreferencesFromCache(context, name) 方法,你可以在特定时机主动释放 Preferences 占用的内存------比如在不需要某个存储实例的时候,调这个方法来释放内存资源。对于长时间运行的后台 Service,这是个有用的优化点。
数据隐私与安全
Preferences 的数据是存储在应用私有目录下的。这意味着其他应用无法访问你的 Preferences 数据。在 HarmonyOS 的安全模型下,每个应用有自己独立的沙箱环境,Preferences 文件就在这个沙箱里。
但有一个注意事项:Preferences 不是加密存储的。 如果你存的是明文密码、密钥、Token 等敏感信息,Preferences 不合适------因为 XML 文件里就是原始文本。敏感信息请用 HarmonyOS 的安全存储方案(如 @kit.ArkData 里提供的加密存储能力,或者使用密钥管理服务)。
所以在设计数据模型的时候,建议做个简单的"脱敏检查":问自己一句,"这个值如果被别人看到了会有什么问题?" 如果是"深色模式是否开启"、"字体大小"这类信息,随便看,没问题。如果是"用户自动登录的 Token",那就得用加密方案了。
总结一下 Preferences 的核心原理:
- 数据是 XML 文件,存储在应用私有沙箱目录下
- 加载时全量读入内存,后续读取不碰磁盘------读取接近于"白嫖"
- 写入走内存缓存 + flush 触发全量刷盘------小数据高效,大数据吃力
- 线程安全------多任务并发不用太担心,但注意实例唯一性
- 非加密------敏感信息另寻他法
这套设计思路其实在移动开发领域不是 HarmonyOS 首创的------Android 有 SharedPreferences,本质上和这个如出一辙。但 HarmonyOS 在这个基础上做了一些优化,比如 API 更简洁、类型安全更好、异步模型更一致。一句话总结:站在前人的肩膀上,做了一些更适合鸿蒙生态的调整。
六、常见问题解答
Q1: Preferences 能存多少条数据?
A: 官方没有硬性限制条数,但工程上建议控制在 200 条以内,单条数据大小不超过 1MB,整体数据量不超过 10MB。原因就是上文说的------flush 是全量写入,数据多了写入性能会明显下降。超过这个量级,建议上数据库。
Q2: 我能不能存一个对象(Object)进去?
A: 可以直接存数字、字符串、布尔值、数组这些基础类型。如果要存复杂的嵌套对象,建议用 JSON.stringify() 转成字符串再存,读的时候用 JSON.parse() 转回来。比如:
typescript
const userConfig = { theme: 'dark', lineSpacing: 1.5 };
await prefs.put('userConfig', JSON.stringify(userConfig));
const raw = await prefs.get('userConfig', '{}');
const config = JSON.parse(raw);
这是一种很常见的"曲线救国"方案。有约 5-10% 的小性能开销,但对于配置项完全够用。
Q3: flush 忘记调了会发生什么?
A: 这是个好问题。如果你调了 put 但没调 flush,而 App 在这个间隙被系统杀死了(比如后台被回收、崩溃了),那内存里缓存的修改就会丢失。所以"先 put 后 flush"应该形成肌肉记忆。不过从 API 12 开始,Preferences 也提供了自动刷盘的选项,但手动 flush 更可控。
Q4: 如果我同时在多个页面读写同一个 Preferences 会冲突吗?
A: 线程安全层面,系统做了锁保护,不会出现数据错乱。但可能出现"脏读"------比如 A 页面写入了一个值但还没 flush,B 页面读到的还是旧值。通常影响不大,因为 flush 很快。如果你的场景对实时一致性要求高(比如 A 写入后 B 必须立刻读到),那可以考虑在读取前主动调一次 flush。
Q5: Preferences 和数据库可以混用吗?
A: 当然可以,而且这是很常见的设计。比如一个笔记 App:笔记的正文和列表用数据库存(数据量大、需要搜索),而用户偏好的主题、字体、排序方式用 Preferences 存。两者各司其职,互不干扰。这也是架构设计里"使用合适的工具做合适的事"的体现。
Q6: 我的 Preferences 文件在哪?能自己手动改吗?
A: 在应用沙箱目录下的某个路径,具体是由系统管理的。不建议也不应该手动去修改这个文件------首先你还需要设备 root 权限才能访问到沙箱目录,其次手动修改可能会破坏文件格式,导致整个 Preferences 实例加载失败。所有的读写操作通过 API 来完成就好,别去动那个文件。
Q7: Preferences 能存图片或者文件吗?
A: 不能。Preferences 只能存字符串、数字(整数和小数)、布尔值这些"原子"类型。如果你想存一张图片、一个 PDF、一段录音,那就不是 Preferences 该干的活了------得用文件存储(通过 file.fs 相关 API)或者数据库。Preferences 的定位非常明确:小数据、简单值、键值对。别让它干超出能力范围的事。
七、运行效果
下面我们用 ASCII 字符画来模拟 App 的运行效果。想象你打开了一个阅读 App,第一件事就是加载之前保存的设置:

当用户点击"保存设置"按钮时,发生的事情:

然后第二天你再次打开 App:

你看,从用户的角度来说,一切都很自然------我昨天怎么设置的,今天打开还那样。这种体验的好,是用户说不出来但能感受到的。那些让你"每次都要重新设一遍"的 App,为什么觉得不好用?就是因为没有做数据持久化。
八、扩展方向
到这儿,Preferences 的核心内容基本都讲完了。但学习和实践的边界不止于此,我列几个可以继续深入的方向,给你一些灵感。
1. 与 UI 状态管理联动
Preferences 存的只是"数据",怎么把数据和 UI 连接起来?在 HarmonyOS 开发里,你可以结合 @State 和 @Link 装饰器,让数据变化自动刷新界面。更进阶一点,可以用 AppStorage 配合 Preferences 做全局状态同步------AppStorage 是 ArkUI 提供的应用级状态存储,它和 Preferences 的区别是:AppStorage 只在运行时存在,App 一关就没了;Preferences 是持久化的。两者结合的方式是:App 启动时从 Preferences 读数据,写入 AppStorage;用户修改时,AppStorage 变 → 同步到 Preferences → flush。
2. 多用户场景的隔离
如果你的 App 支持多账号切换,那每个账号的偏好设置不应该混在一起。可以在 Preferences 的存储名字上做文章:比如 'reading_prefs_user_001' 和 'reading_prefs_user_002',每个用户一个独立的存储实例。切换账号的时候,切换对应的 Preferences 实例就行了。
3. 配合云备份
Preferences 数据存在本地,但如果用户换手机了,这些偏好就丢了。想要更好的体验,可以考虑把 Preferences 中的数据同步到云端------在用户切换设备的时候,从云端拉取偏好设置,恢复到本地。HarmonyOS 的 @kit.ArkData 提供了 KVStore 的分布式能力,如果你真的需要多端同步,可以从 Preferences 平滑迁移到 KVStore------因为它们的数据模型(键值对)是一样的,迁移成本不高。
4. 性能监控
在开发阶段,可以在 Preferences 的读写操作前后加上时间戳,统计每次操作的耗时。如果在真机测试时发现 flush 耗时超过 100ms,那就该考虑是不是 Preferences 里数据太多了,或者检查单条数据是不是太大了。保持对性能的敏感,是做好"轻量"的关键。
5. 监听数据变更做更丰富的业务逻辑
前面提到了 on('change') 的用法,但这个能力的想象空间其实挺大的。举个例子:你的阅读 App 有个"每日阅读提醒"功能,要求在字体大小改到"超大"时弹出提示"考虑减少阅读时间以保护视力"。通过 Preferences 的变更监听,你可以在 fontSize 改变的那一刻触发这个逻辑------不需要轮询、不需要手动判断。
6. 多 Process 并发写入
如果你的 App 使用了多进程架构(比如在主进程和后台 Service 进程里分别读写 Preferences),需要注意:不同进程各自维护自己的 Preferences 实例,不会自动同步。这种情况下建议用数据库做存储,或者通过 IPC(进程间通信)让所有写入操作都通过主进程完成。
结语
聊到这里,关于 Preferences 轻量存储的话题就到尾声了。你可以发现,它其实没有太多花哨的东西------一个结构简单、API 极简、性能出色的本地存储方案,专门处理"小数据"的场景。它不追求大而全,而是在自己擅长的领域里做到极致。
这其实也是写代码的一个道理:不是每个问题都需要一个复杂的解决方案。有时候,最简单直接的方案就是最好的方案。Preferences 这样的方案,就像一个可靠的伙伴------平时安静地待在后台,不刷存在感,但每次你需要它的时候,它都在那里。
下次你的 App 需要存用户的偏好了,记得想到它。