鸿蒙新特性:@ohos.data.preferences 首选项存储实验室实战 —— 轻量级键值对持久化与变更监听
引言
在移动应用开发中,持久化用户配置是最常见的需求之一——主题偏好、字体大小、自动保存开关、最后登录时间,这些数据量小但读频繁,不适合每次都读写文件,更不需要数据库。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 SharedPreferences 的 apply()(异步写磁盘,可能在你最需要时还没完成)不同,鸿蒙的 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 变更监听的实际用途
变更监听最常见的场景有两个:
- 跨页面配置同步:页面 A 修改了主题设置,页面 B 通过
on('change')立即感知并更新 UI - 调试与审计:在开发阶段注册全局监听,追踪所有配置变更的来源和时间
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 设计了六个功能区域:
-
状态栏:显示 Preferences 加载状态(就绪/加载中)和变更监听开关状态(ON/OFF),变更监听开关本身也是一个 Toggle 按钮
-
写入键值面板:提供键名
TextInput+ 值TextInput+ 类型选择器(string/number/boolean 三个切换按钮)+ "保存"按钮。类型选择器使用互斥高亮设计——选中类型显示实心背景,未选中类型显示浅色背景 -
读取键值面板:提供键名
TextInput+ "读取"按钮 + 结果显示行。读取不存在的键时显示 “(不存在)” 并采用灰色文字 -
已存储键值列表:展示所有已知键值对(键名 + 值 + 类型标签 + 读取/删除按钮)。每个条目显示三部分信息:键名(加粗)、值(等宽字体、单行省略)、类型标签(彩色胶囊)。每个条目右侧有两个操作按钮——“读取”(紫色边框)和"删除"(红色边框)
-
API 能力说明:两个段落分别介绍核心 API 和使用限制,作为知识参考卡片
-
操作日志:毫秒级时间戳 + 操作描述 + 颜色分类(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初始值,所有同步操作前先检查非 nullkvList在每次写入、删除、变更事件后通过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 提供四个核心交互点:
-
写入键值对:输入键名 → 选择类型(string/number/boolean)→ 输入值 → 点击"保存" → 键值对出现在已存储列表中 → 操作日志记录
-
读取键值对:输入键名 → 点击"读取" → 结果显示在读取面板中(存在显示值,不存在显示灰色"(不存在)")→ 操作日志记录
-
管理已存储键值:列表中每个条目显示类型标签(紫色胶囊),右侧提供"读取"按钮(填充读取面板)和"删除"按钮(移除该键值对)→ 操作后列表自动更新
-
变更监听开关:状态栏中的 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 中轻量级配置持久化的首选方案。通过本文的学习,你应该已经掌握:
- 实例获取:
getPreferences(context, name): Promise<Preferences>异步加载,返回 Preferences 实例 - 数据写入:
putSync(key, value)写入键值对,flushSync()强制落盘,ValueType 支持 9 种类型 - 数据读取:
getSync(key, defValue)读取键值(不存在返回默认值),hasSync(key)检查存在性 - 数据管理:
deleteSync(key)删除单键,on/off('change')注册/移除变更监听 - 使用限制:
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 方法同步执行,读取操作无需任何权限,数据在应用卸载时自动清除。
更多推荐



所有评论(0)