引言

在移动应用开发中,持久化用户配置是最常见的需求之一——主题偏好、字体大小、自动保存开关、最后登录时间,这些数据量小但读频繁,不适合每次都读写文件,更不需要数据库。HarmonyOS NEXT 提供了 @ohos.data.preferences 模块作为轻量级键值对的持久化存储方案。

@ohos.data.preferences 属于 @kit.ArkData,是一个经典的 Key-Value 存储实现。它的 API 设计可以总结为一句话:获取实例是异步的(Promise),操作数据是同步的(Sync)。存储的数据类型覆盖了 9 种 ValueType — 从基础的 number/string/boolean 到复杂的 Array/Uint8Array/object/bigint,能够满足绝大多数配置存储需求。数据自动持久化为沙箱内的 XML 文件,应用卸载时被系统清除,无需额外权限。

与 Android 的 SharedPreferences(有 apply/commit 两种写入模式和已知的性能陷阱)和 iOS 的 UserDefaults(Foundation 框架,自动持久化但类型支持有限)不同,鸿蒙的 Preferences 采用了更现代的设计:显式的 flushSync() 控制落盘时机,on/off('change') 事件监听数据变更,deleteSync 单一键删除而非必须整个清除。这种"精确控制 + 事件驱动"的组合让开发者可以灵活地平衡性能和数据一致性。

本文将深入讲解 @ohos.data.preferences 的核心 API、ValueType 体系、变更监听机制和实际使用限制,并构建一个"首选项存储实验室"Demo——在页面上完成键值对的写入、读取、删除和实时监听的全流程操作。

一、API 架构:异步获取实例 + 同步操作数据

1.1 核心设计理念

@ohos.data.preferences 的设计核心是 Promise 获取实例 + 同步操作数据 的双模式。导入方式:

import preferences from '@ohos.data.preferences';

所有数据操作都通过 Preferences 实例进行,但获取这个实例需要异步调用 getPreferences()

// 异步获取实例
const ctx = getContext(this);
preferences.getPreferences(ctx, 'my_config').then((prefs: preferences.Preferences) => {
  // 之后所有操作都是同步的
  prefs.putSync('theme', 'dark');
  prefs.flushSync();
});

这种设计的原因在于:Preferences 实例的创建需要从磁盘加载已有的 XML 文件,而文件 I/O 操作(哪怕是 XML 解析这样的小文件)必须在异步上下文中执行,避免阻塞 UI 线程。但一旦实例加载完成,内存中的读写操作就可以同步执行了。

1.2 Preferences 实例的完整 API 列表

通过 getPreferences() 获取到的 Preferences 实例提供以下方法:

方法 返回类型 说明
putSync(key: string, value: ValueType) void 写入键值对
getSync(key: string, defValue: ValueType) ValueType 读取键值(不存在时返回默认值)
hasSync(key: string) boolean 检查键是否存在
deleteSync(key: string) void 删除指定键
flushSync() void 强制将内存数据写入磁盘 XML 文件
on('change', callback) void 注册数据变更监听
off('change', callback?) void 移除数据变更监听

所有 Sync 方法都是同步的,不会抛出 Promise——但如果传入非法参数(如键名超过 80 字符)会直接抛出异常,需要通过 try/catch 保护。

1.3 ValueType 的 9 种支持类型

ValueType 是 Preferences 支持的数据类型联合类型:

type ValueType = number | string | boolean | Array<number> | Array<string> |
                 Array<boolean> | Uint8Array | object | bigint;

这意味着你可以存储几乎任何常见的数据类型:

// 字符串
prefs.putSync('username', 'Alice');

// 数字
prefs.putSync('fontSize', 16);

// 布尔
prefs.putSync('autoSave', true);

// 对象(会被序列化)
prefs.putSync('userProfile', { name: 'Alice', age: 25, vip: true });

// bigint
prefs.putSync('maxId', BigInt(9999999999999));

// Uint8Array(二进制数据)
prefs.putSync('avatar', new Uint8Array([0x89, 0x50, 0x4E, 0x47]));

需要注意的是,object 类型的存储依赖序列化——存储时使用 JSON 序列化,读取时反序列化。复杂的对象(例如包含函数、Symbol、循环引用)不会被正确保留。getSync 读取 object 类型时会自动反序列化为 JavaScript 对象。

1.4 键名与值的长度限制

Preferences 有两个硬性常量限制:

  • MAX_KEY_LENGTH = 80 — 键名最大 80 字符
  • MAX_VALUE_LENGTH = 8192 — 字符串值最大 8192 字符

这些限制确保了单个键值对的内存开销可控。如果需要存储超过 8192 字符的文本,应该使用 @ohos.file.fs 的文件读写而非 Preferences。

二、数据持久化:flushSync 与自动落盘

2.1 flushSync 的明确性

flushSync() 是控制数据何时写入磁盘的关键方法。它强制将内存中的键值数据序列化为 XML 并写入沙箱内的文件。

prefs.putSync('theme', 'dark');
prefs.putSync('fontSize', 14);
prefs.flushSync(); // 此时才真正写入磁盘

不调用 flushSync() 时,数据仅存在内存中,应用崩溃或系统杀死进程可能导致数据丢失。因此,在写入关键配置后应该显式调用 flushSync()

与 Android SharedPreferencesapply()(异步写磁盘,可能在你最需要时还没完成)不同,鸿蒙的 flushSync() 是同步的——调用后数据一定在磁盘上。这是一个语义上更清晰的设计:开发者不会混淆"我写了数据"和"数据在磁盘上"这两个状态。

2.2 数据的磁盘存储位置

Preferences 的数据文件存储在应用沙箱内的 /data/storage/el2/base/preferences/ 目录下,文件名由 getPreferences() 的第二个参数决定:

preferences.getPreferences(ctx, 'lab_prefs'); // → lab_prefs.xml

文件格式为 XML:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<preferences>
  <string name="username">Alice</string>
  <int name="fontSize" value="16"/>
  <boolean name="autoSave" value="true"/>
</preferences>

打开这个文件可以让开发者直接查看和验证存储的数据——这是 Preferences 相比数据库存储的一大优势:数据可读、可手动修复、可调试。

三、变更监听机制:on/off(‘change’)

3.1 注册与移除变更监听

on('change', callback) 提供了一个事件驱动的数据变更通知机制。当任何键值对被修改(新增、更新、删除)时,回调函数会被调用:

// 注册监听
prefs.on('change', (data: { key: string }) => {
  console.log('键值已变更: ' + data.key);
  // data.key 是发生变更的键名
});

// 移除监听
prefs.off('change');

注意 off('change') 可以不传回调——这会移除该事件上的所有监听器。如果需要精确控制,可以在 on 时保存回调引用,off 时传入同一个引用。

3.2 变更监听的实际用途

变更监听最常见的场景有两个:

  1. 跨页面配置同步:页面 A 修改了主题设置,页面 B 通过 on('change') 立即感知并更新 UI
  2. 调试与审计:在开发阶段注册全局监听,追踪所有配置变更的来源和时间

Demo 中的使用模式:开启监听后,每次写入或删除操作都会触发 change 事件,日志中即时显示变更信息。

private toggleListener(): void {
  if (this.listenOn) {
    this.prefs.off('change');
    this.listenOn = false;
  } else {
    this.prefs.on('change', () => {
      this.addLog('Preferences 数据已变更 (change 事件)', 'success');
      this.loadAllKeys(); // 自动刷新已存储键值列表
    });
    this.listenOn = true;
  }
}

在这里插入图片描述
在这里插入图片描述

四、实战 Demo:首选项存储实验室

4.1 页面设计

"首选项存储实验室"页面围绕 Preferences 的核心 API 设计了六个功能区域:

  1. 状态栏:显示 Preferences 加载状态(就绪/加载中)和变更监听开关状态(ON/OFF),变更监听开关本身也是一个 Toggle 按钮

  2. 写入键值面板:提供键名 TextInput + 值 TextInput + 类型选择器(string/number/boolean 三个切换按钮)+ "保存"按钮。类型选择器使用互斥高亮设计——选中类型显示实心背景,未选中类型显示浅色背景

  3. 读取键值面板:提供键名 TextInput + "读取"按钮 + 结果显示行。读取不存在的键时显示 “(不存在)” 并采用灰色文字

  4. 已存储键值列表:展示所有已知键值对(键名 + 值 + 类型标签 + 读取/删除按钮)。每个条目显示三部分信息:键名(加粗)、值(等宽字体、单行省略)、类型标签(彩色胶囊)。每个条目右侧有两个操作按钮——“读取”(紫色边框)和"删除"(红色边框)

  5. API 能力说明:两个段落分别介绍核心 API 和使用限制,作为知识参考卡片

  6. 操作日志:毫秒级时间戳 + 操作描述 + 颜色分类(success 绿色、error 红色、system 灰色)

4.2 核心实现

状态模型设计

@State prefsReady: boolean = false;      // Preferences 是否已加载
@State writeKey: string = 'username';    // 写入键名(默认值)
@State writeValue: string = 'Alice';     // 写入值(默认值)
@State writeType: string = 'string';     // 写入类型(string/number/boolean)
@State readKey: string = '';             // 读取键名
@State readResult: string = '--';        // 读取结果
@State kvList: KvEntry[] = [];           // 已存储的键值列表
@State listenOn: boolean = false;        // 变更监听开关
@State logs: LogEntry[] = [];            // 操作日志
private prefs: preferences.Preferences | null = null;

设计要点:

  • prefsReady 区分加载中的异步阶段与就绪后的同步操作阶段
  • prefs 使用 null 初始值,所有同步操作前先检查非 null
  • kvList 在每次写入、删除、变更事件后通过 loadAllKeys() 重新扫描更新

异步加载 + 同步操作模式

aboutToAppear(): void {
  const ctx = getContext(this);
  preferences.getPreferences(ctx, 'lab_prefs').then((p: preferences.Preferences) => {
    this.prefs = p;
    this.prefsReady = true;
    this.addLog('Preferences 已加载: lab_prefs', 'success');
    this.loadAllKeys();
  }).catch((e: Error) => {
    this.addLog('加载 Preferences 失败: ' + e.message, 'error');
  });
}

这是整个页面的核心模式——getPreferences() 是异步的,在 Promise resolve 后设置 prefsReady = true,之后所有用户交互(写入、读取、删除)都使用 putSync/getSync/deleteSync/flushSync 同步方法。

写入带类型选择

private saveValue(): void {
  if (this.prefs === null) return;
  const key = this.writeKey.trim();
  let value: preferences.ValueType;
  switch (this.writeType) {
    case 'number':
      value = parseFloat(this.writeValue);
      if (isNaN(value)) { this.addLog('请输入有效数字', 'error'); return; }
      break;
    case 'boolean':
      value = this.writeValue.toLowerCase() === 'true';
      break;
    default:
      value = this.writeValue;
      break;
  }
  this.prefs.putSync(key, value);
  this.prefs.flushSync();
  this.addLog('已保存: ' + key + ' = ' + value.toString(), 'success');
  this.loadAllKeys();
}

类型转换逻辑:number 用 parseFloat 并验证 NaN,boolean 判断字符串是否为 “true”,string 直接使用原值。转换后的值作为 ValueType 传入 putSync

键值列表扫描

private loadAllKeys(): void {
  if (this.prefs === null) return;
  this.kvList = [];
  const testKeys: string[] =
    ['username', 'theme', 'fontSize', 'autoSave', 'lastLogin', 'score'];
  for (let i = 0; i < testKeys.length; i++) {
    try {
      const val = this.prefs.getSync(testKeys[i], '');
      if (val !== '' && val !== undefined && val !== null) {
        this.kvList.push({
          key: testKeys[i],
          value: val.toString(),
          vtype: typeof val
        });
      }
    } catch (e) {
      // key doesn't exist, skip
    }
  }
}

Demo 使用预定义的测试键列表来扫描已存储的键值对——这是一个简化方案。在生产代码中,Preferences 本身不提供 “列出所有键” 的 API,因此你需要在应用层维护一个已使用键名的清单,或者使用其他方式追踪。

变更监听 Toggle

private toggleListener(): void {
  if (this.prefs === null) return;
  if (this.listenOn) {
    this.prefs.off('change');
    this.listenOn = false;
    this.addLog('变更监听已关闭', 'system');
  } else {
    this.prefs.on('change', () => {
      this.addLog('Preferences 数据已变更 (change 事件)', 'success');
      this.loadAllKeys();
    });
    this.listenOn = true;
    this.addLog('变更监听已开启', 'success');
  }
}

4.3 交互方式

Demo 提供四个核心交互点:

  1. 写入键值对:输入键名 → 选择类型(string/number/boolean)→ 输入值 → 点击"保存" → 键值对出现在已存储列表中 → 操作日志记录

  2. 读取键值对:输入键名 → 点击"读取" → 结果显示在读取面板中(存在显示值,不存在显示灰色"(不存在)")→ 操作日志记录

  3. 管理已存储键值:列表中每个条目显示类型标签(紫色胶囊),右侧提供"读取"按钮(填充读取面板)和"删除"按钮(移除该键值对)→ 操作后列表自动更新

  4. 变更监听开关:状态栏中的 Toggle 切换,开启后任何写入/删除操作都会触发 change 事件 → 日志中显示"Preferences 数据已变更"→ 列表自动刷新

五、最佳实践与使用限制

5.1 何时使用 Preferences

Preferences 最合适的场景是:

  • 用户配置:主题模式(dark/light)、字体大小、通知开关、自动保存等
  • 应用状态:首次启动标记、上次登录用户名、引导完成的步骤
  • 轻量缓存:最近搜索关键词、上次选择的 Tab 页、界面布局偏好

不适合使用 Preferences 的场景:

  • 大量数据(> 100 个键值对):考虑使用 @ohos.data.relationalStore(关系数据库)或文件存储
  • 大文本或二进制(> 8KB 单值):使用 @ohos.file.fs 文件存储
  • 复杂查询(按条件筛选/排序):使用数据库而非遍历所有键
  • 敏感数据(密码、Token):使用 @ohos.security.huks(通用密钥库)加密存储

5.2 同步操作中的异常处理

虽然 Sync 方法使用方便,但它们在错误条件下会直接抛出异常。关键的保护点:

try {
  prefs.putSync(key, value);
  prefs.flushSync();
} catch (e) {
  console.error('写入失败: ' + (e as Error).message);
  // 可能的原因:键名超过 80 字符、值超过 8192 字符、磁盘空间不足
}

Demo 中所有操作都包裹在 try/catch 中——这在 Preferences 的使用中是必要的,因为文件 I/O 错误(磁盘满、权限变更)可能在运行时发生。

5.3 生命周期管理

Preferences 实例不需要手动释放——它在 getPreferences() 返回后由系统管理。但变更监听器需要在组件销毁时移除,避免内存泄漏:

aboutToDisappear(): void {
  if (this.listenOn && this.prefs !== null) {
    try {
      this.prefs.off('change');
    } catch (e) {
      // ignore
    }
  }
}

如果不移除监听器,当组件被销毁后,系统仍然会维持对回调函数的引用——造成内存泄漏和潜在的无效回调调用。

5.4 数据迁移与版本管理

当应用升级时,Preferences 文件保留在沙箱内不变。如果你需要修改配置的键名或值格式,应该在应用初始化时做迁移:

preferences.getPreferences(ctx, 'my_config').then((prefs) => {
  if (!prefs.hasSync('version') || prefs.getSync('version', 1) < 2) {
    // 执行从 v1 到 v2 的数据迁移
    const oldTheme = prefs.getSync('theme', 'light');
    prefs.putSync('uiTheme', oldTheme); // 重命名键
    prefs.deleteSync('theme');
    prefs.putSync('version', 2);
    prefs.flushSync();
  }
});

六、总结

@ohos.data.preferences 是 HarmonyOS NEXT 中轻量级配置持久化的首选方案。通过本文的学习,你应该已经掌握:

  1. 实例获取getPreferences(context, name): Promise<Preferences> 异步加载,返回 Preferences 实例
  2. 数据写入putSync(key, value) 写入键值对,flushSync() 强制落盘,ValueType 支持 9 种类型
  3. 数据读取getSync(key, defValue) 读取键值(不存在返回默认值),hasSync(key) 检查存在性
  4. 数据管理deleteSync(key) 删除单键,on/off('change') 注册/移除变更监听
  5. 使用限制MAX_KEY_LENGTH = 80 字符键名限制,MAX_VALUE_LENGTH = 8192 字符串值限制,数据以 XML 存储在应用沙箱内

@ohos.data.preferences 的最佳使用模式可以总结为:

getPreferences 异步获取实例 → 所有操作使用 Sync 同步方法 → flushSync 确保数据落盘 → on/off 管理变更监听 → aboutToDisappear 中移除监听避免内存泄漏。所有操作 try/catch 保护,写入后显式 flushSync,组件销毁时 off 清理。

在 HarmonyOS 的数据管理体系中,@ohos.data.preferences 定位于"轻量级配置存储",与 @ohos.data.relationalStore(关系数据库)、@ohos.data.distributedKVStore(分布式键值数据库)和 @ohos.file.fs(文件存储)形成完整的数据能力栈。它是应用设置和用户偏好存储的第一选择——简单、可靠、零权限、开箱即用。

@ohos.data.preferences 属于 @kit.ArkData,获取实例需要 context,所有写操作使用 Sync 方法同步执行,读取操作无需任何权限,数据在应用卸载时自动清除。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐