在这里插入图片描述

一、我们想解决什么问题

咱们先聊个真实的场景。

你打开一个阅读类 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。这个面板包含以下几种设置项:

  1. 深色模式开关 —— 用户是否启用了深色主题。这个值只有两种状态:开或关。用布尔值(boolean)最合适。
  2. 字体大小 —— 用户选择的字号。可能是"小"“中”“大”"超大"这几个选项。用字符串(string)存最方便。
  3. 上次阅读位置 —— 用户上次看到哪一页。比如第 128 页。这是一个数字,用 number 类型。
  4. 用户昵称 —— 用户在 App 里设置的自定义名称。这个是个字符串。

四个设置项,四种不同的数据类型,刚好能覆盖日常使用的绝大多数场景。

用 TypeScript 定义数据模型

在 HarmonyOS 开发里,我们通常用 TypeScript 的 interface 来定义数据结构。这就好比搭了个"模板",后面的数据都得按这个模板来。

来看代码:

// 阅读设置的数据模型
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 对象"——通俗说就是"先打开那个存数据的小本本"。

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——英文里就是"放进去"的意思,非常形象。

// 写入一条数据:是否使用深色模式
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

// 读取深色模式开关(默认值设为 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 删除单条与清空

有时候用户想把某个设置恢复到默认状态——比如重置字体大小。这时候就需要删除数据了。

// 删除单条数据
await prefs.delete('lastPage');
await prefs.flush();

// 或者整个清空——就像恢复出厂设置
await prefs.clear();
await prefs.flush();

delete 是删一条;clear 是把整个 reading_prefs 里的数据全清空。用哪个取决于你的场景——是想"删掉一个设置",还是"把用户的所有偏好恢复默认"。

HarmonyOS 还有一个跟 Preferences 配合得很好的机制:持久化订阅。你可以监听某个 Key 的变化,比如当用户改了深色模式,你可以自动更新 UI:

prefs.on('change', (key: string) => {
  if (key === 'darkMode') {
    // 自动切换主题
    applyTheme(prefs.get('darkMode', false));
  }
});

但要注意,这个监听器是比较轻量的通知机制,不是事件总线——别在上面挂太多复杂逻辑。而且从 API 12 开始,建议用 PreferencesChangeCallback 配合 on('multiSettingsChange') 来批量处理变更,性能和可维护性更好。这里先提一句,不展开,后续深入聊监听机制的时候再说。

4.5 完整的使用函数

最后,我把上面这些操作整合成一个完整的使用示例,方便你直接复制过去跑:

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 语句,没有建表的过程,没有事务管理——就是 putgetdeleteflush 四个操作,来回组合。这就是 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 的核心原理:

  1. 数据是 XML 文件,存储在应用私有沙箱目录下
  2. 加载时全量读入内存,后续读取不碰磁盘——读取接近于"白嫖"
  3. 写入走内存缓存 + flush 触发全量刷盘——小数据高效,大数据吃力
  4. 线程安全——多任务并发不用太担心,但注意实例唯一性
  5. 非加密——敏感信息另寻他法

这套设计思路其实在移动开发领域不是 HarmonyOS 首创的——Android 有 SharedPreferences,本质上和这个如出一辙。但 HarmonyOS 在这个基础上做了一些优化,比如 API 更简洁、类型安全更好、异步模型更一致。一句话总结:站在前人的肩膀上,做了一些更适合鸿蒙生态的调整。


六、常见问题解答

Q1: Preferences 能存多少条数据?

A: 官方没有硬性限制条数,但工程上建议控制在 200 条以内,单条数据大小不超过 1MB,整体数据量不超过 10MB。原因就是上文说的——flush 是全量写入,数据多了写入性能会明显下降。超过这个量级,建议上数据库。

Q2: 我能不能存一个对象(Object)进去?

A: 可以直接存数字、字符串、布尔值、数组这些基础类型。如果要存复杂的嵌套对象,建议用 JSON.stringify() 转成字符串再存,读的时候用 JSON.parse() 转回来。比如:

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 需要存用户的偏好了,记得想到它。


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐