HarmonyOS 数据持久化:Preferences 轻量级键值存储完全指南


一、引言
在移动应用开发中,数据持久化是一个绕不开的话题。无论是用户偏好设置、登录状态、主题模式,还是简单的应用配置,都需要一种轻量、高效、可靠的存储方案。HarmonyOS NEXT 为我们提供了多种数据持久化方案,其中 Preferences 是最轻量、最易用的一种。
本文将以一个完整的实战案例为主线,深入讲解 HarmonyOS 中 Preferences 键值存储的使用方法、底层原理、最佳实践以及常见陷阱。全文以代码为主,配合逐行说明,帮助读者彻底掌握这一技术方向。
二、为什么需要数据持久化
2.1 内存数据与持久化数据的区别
在讲解 Preferences 之前,我们先明确一个基本概念:应用运行时的数据默认是保存在内存中的。内存中的数据具有以下特点:
- 易失性:应用进程被杀死、系统重启、内存回收时,数据会丢失。
- 快速访问:内存读写速度极快,适合频繁访问的临时数据。
- 容量有限:内存资源宝贵,不能无限存储。
而持久化数据则存储在磁盘上,具有以下特点:
- 持久性:应用重启后数据依然存在。
- 访问较慢:磁盘 I/O 比内存慢,需要合理设计读写策略。
- 容量较大:磁盘空间远大于内存。
2.2 典型应用场景
在 HarmonyOS 应用中,以下场景非常适合使用 Preferences:
- 用户偏好设置:主题颜色、字体大小、语言选择等。
- 登录状态:记住用户名、登录令牌、会话信息。
- 应用配置:首次启动标记、引导页是否展示过、缓存策略配置。
- 简单的业务数据:游戏最高分、阅读进度、草稿内容等。
三、Preferences 核心概念
3.1 什么是 Preferences
Preferences 是 HarmonyOS 提供的一种轻量级键值(Key-Value)存储组件,专门用于存储少量数据。它的设计哲学是"小而快",适合存储配置类数据,但不适合存储大量结构化数据(那种场景应该使用关系型数据库 RelationalStore)。
Preferences 的主要特点:
- 键值对存储:数据以 Key-Value 形式组织,Key 是字符串,Value 支持 string、number、boolean 等基本类型。
- 异步 API:所有操作都是异步的,避免阻塞 UI 线程。
- 实例隔离:不同实例之间数据相互隔离,互不影响。
- 落盘机制:支持手动 flush 和自动落盘。
3.2 数据模型
Preferences 的数据模型非常简单,可以理解为一张只有两列的表格:
| Key | Value |
|---|---|
| user_name | 张三 |
| theme_mode | dark |
| login_count | 5 |
每个 Preferences 实例对应一个独立的存储文件,文件名的后缀固定为 .preferences。
四、实战代码:完整的数据持久化页面
下面我们来实现一个完整的数据持久化演示页面。这个页面采用极简黑白风格,包含数据展示表格、写入表单和操作按钮。
4.1 导入依赖
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
代码说明:
@kit.ArkData:数据管理能力套件,提供了 Preferences、关系型数据库、分布式数据库等数据管理 API。@kit.AbilityKit:能力套件,这里用来获取 UIAbilityContext,因为获取 Preferences 实例需要传入上下文。@kit.PerformanceAnalysisKit:性能分析套件,提供了 hilog 日志打印能力,用于调试和错误追踪。
4.2 定义数据结构
interface KeyValueRow {
key: string;
value: string;
}
代码说明:
我们定义了一个 KeyValueRow 接口,用于描述表格中的一行数据。它包含两个字段:
key:键名,用于唯一标识一条数据。value:键值,存储实际的数据内容。
使用接口而非直接使用对象字面量,是为了保证类型安全,符合 ArkTS 严格类型检查的要求。
4.3 组件状态与成员变量
@Entry
@Component
struct DataPersistencePage {
@State rows: KeyValueRow[] = [
{ key: 'user_name', value: '未设置' },
{ key: 'theme_mode', value: '未设置' },
{ key: 'login_count', value: '未设置' }
];
@State inputKey: string = '';
@State inputValue: string = '';
private prefs?: preferences.Preferences;
代码说明:
@Entry:标记该组件为页面入口组件,可以被路由跳转。@Component:标记该结构体为自定义组件。@State rows:使用@State装饰器声明响应式数据,当 rows 变化时,UI 会自动刷新。这里初始化了三条默认数据,对应三个键。@State inputKey/@State inputValue:分别绑定输入框的键名和键值。private prefs:保存 Preferences 实例的引用。使用preferences.Preferences类型,并用可选类型?标记,因为实例在异步获取之前是未定义的。
4.4 生命周期方法:加载数据
aboutToAppear(): void {
this.loadPrefs();
}
async loadPrefs(): Promise<void> {
try {
const ctx = getContext(this) as common.UIAbilityContext;
this.prefs = await preferences.getPreferences(ctx, 'myStore');
const name = await this.prefs.get('user_name', '未设置');
const theme = await this.prefs.get('theme_mode', '未设置');
const count = await this.prefs.get('login_count', 0);
this.rows = [
{ key: 'user_name', value: `${name}` },
{ key: 'theme_mode', value: `${theme}` },
{ key: 'login_count', value: `${count}` }
];
} catch (e) {
hilog.error(0x0000, 'Prefs', 'load failed %{public}s', JSON.stringify(e));
}
}
代码说明:
aboutToAppear 是组件的生命周期方法,在组件即将显示时调用。在这里我们调用 loadPrefs 方法加载已存储的数据。
loadPrefs 方法的核心逻辑:
-
获取上下文:
getContext(this) as common.UIAbilityContext获取当前组件的上下文对象。getContext是全局方法,返回组件的上下文,我们将其断言为UIAbilityContext类型,因为getPreferences需要这个类型的参数。 -
获取 Preferences 实例:
preferences.getPreferences(ctx, 'myStore')是异步方法,返回一个 Promise。第一个参数是上下文,第二个参数是存储文件名(不需要带后缀)。如果文件不存在,会自动创建。这里我们使用await等待实例获取完成。 -
读取数据:
this.prefs.get('user_name', '未设置')从存储中读取键为user_name的值,如果不存在则返回默认值'未设置'。get方法同样返回 Promise,需要await。 -
更新 UI:将读取到的数据重新组装为
rows数组,由于rows是@State数据,赋值后 UI 会自动刷新。 -
异常处理:使用
try...catch捕获可能出现的异常,并通过hilog.error打印错误日志。%{public}s是日志格式化占位符,表示公开字符串(不涉及隐私)。
4.5 保存数据
async saveRow(key: string, value: string): Promise<void> {
if (!this.prefs || !key.trim()) {
return;
}
try {
await this.prefs.put(key.trim(), value.trim());
await this.prefs.flush();
await this.loadPrefs();
} catch (e) {
hilog.error(0x0000, 'Prefs', 'save failed %{public}s', JSON.stringify(e));
}
}
代码说明:
saveRow 方法用于将新的键值对写入存储:
-
参数校验:如果 Preferences 实例不存在(
!this.prefs)或者键名为空字符串(!key.trim()),则直接返回,避免无效操作。 -
写入数据:
this.prefs.put(key, value)将键值对写入内存中的 Preferences 实例。注意,此时数据还没有真正写入磁盘,只是写入了内存缓存。 -
强制落盘:
this.prefs.flush()将内存中的数据强制写入磁盘。这是关键一步,如果不调用 flush,数据可能不会立即持久化。 -
刷新界面:调用
loadPrefs()重新读取数据并刷新 UI,让用户看到最新状态。
4.6 清空数据
async clearAll(): Promise<void> {
if (!this.prefs) {
return;
}
try {
await this.prefs.clear();
await this.prefs.flush();
await this.loadPrefs();
} catch (e) {
hilog.error(0x0000, 'Prefs', 'clear failed %{public}s', JSON.stringify(e));
}
}
代码说明:
clearAll 方法清空当前 Preferences 实例中的所有数据:
this.prefs.clear():清空内存中的所有键值对。this.prefs.flush():将清空操作同步到磁盘。this.loadPrefs():刷新 UI,此时所有数据应显示为默认值。
4.7 构建 UI
@Builder
SectionTitle(title: string) {
Text(title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#1B1B1B')
.letterSpacing(2)
.margin({ top: 8, bottom: 8 })
}
代码说明:
@Builder 装饰器用于声明一个 UI 构建函数,可以复用一段 UI 结构。这里的 SectionTitle 接收一个标题字符串参数,返回一个带样式的标题文本。使用 @Builder 可以避免重复编写相同的标题样式代码。
build() {
Scroll() {
Column() {
// 顶部黑区
Column() {
Text('DATA')
.fontSize(12)
.fontColor('#888888')
.letterSpacing(6)
Text('数据持久化')
.fontSize(26)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.margin({ top: 6 })
Text('Preferences 轻量级键值存储')
.fontSize(12)
.fontColor('#AAAAAA')
.margin({ top: 6 })
}
.width('100%')
.padding({ top: 48, bottom: 32 })
.backgroundColor('#111111')
代码说明:
build 方法定义组件的 UI 结构。整体布局采用 Scroll 包裹 Column,支持页面滚动。
顶部是一个黑色背景的标题区域:
Scroll:可滚动容器,当内容超出屏幕时支持滚动查看。Column:垂直布局容器,子组件按从上到下的顺序排列。- 标题区域使用
padding({ top: 48, bottom: 32 })设置内边距,backgroundColor('#111111')设置黑色背景,形成强烈的黑白对比风格。
Column() {
this.SectionTitle('当前存储数据')
// 极简两列表格:左键右值,中间竖线分隔
Column() {
ForEach(this.rows, (row: KeyValueRow) => {
Row() {
Text(row.key)
.fontSize(13)
.fontColor('#666666')
.fontFamily('monospace')
.layoutWeight(1)
Text('|')
.fontColor('#DDDDDD')
Text(row.value)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#111111')
.layoutWeight(1)
.textAlign(TextAlign.End)
}
.width('100%')
.padding({ top: 14, bottom: 14, left: 4, right: 4 })
.border({ width: { bottom: 1 }, color: '#EEEEEE' })
})
}
.width('100%')
.border({ width: 1, color: '#E5E5E5' })
.padding({ left: 16, right: 16 })
代码说明:
这里是数据展示表格的核心实现:
ForEach:循环渲染rows数组中的每一行数据。第一个参数是数据源,第二个参数是渲染函数,第三个参数是键生成器(用于列表复用优化)。Row:水平布局容器,将键名、分隔符、键值放在一行。.layoutWeight(1):设置布局权重,让键名和键值各占一半宽度,实现两端对齐。.fontFamily('monospace'):使用等宽字体显示键名,增强代码风格。.border({ width: { bottom: 1 }, color: '#EEEEEE' }):为每行添加底部边框,形成表格分隔线效果。
this.SectionTitle('写入新键值')
// 输入区
Row({ space: 10 }) {
TextInput({ placeholder: '键 (key)', text: this.inputKey })
.layoutWeight(1)
.height(44)
.backgroundColor('#F5F5F5')
.placeholderColor('#AAAAAA')
.onChange((v: string) => { this.inputKey = v; })
TextInput({ placeholder: '值 (value)', text: this.inputValue })
.layoutWeight(1)
.height(44)
.backgroundColor('#F5F5F5')
.placeholderColor('#AAAAAA')
.onChange((v: string) => { this.inputValue = v; })
}
.width('100%')
代码说明:
输入区包含两个 TextInput 输入框:
TextInput:文本输入组件。placeholder参数设置占位提示文字,text参数绑定当前输入值。.onChange:输入内容变化时的回调,将输入值同步到@State变量,实现数据绑定。- 两个输入框通过
Row({ space: 10 })水平排列,各占一半宽度。
// 黑白按钮组:描边 + 实心
Row({ space: 12 }) {
Button('写入存储')
.height(44)
.layoutWeight(1)
.fontSize(14)
.fontColor(Color.White)
.backgroundColor('#111111')
.borderRadius(0)
.onClick(() => {
this.saveRow(this.inputKey, this.inputValue);
})
Button('清空全部')
.height(44)
.layoutWeight(1)
.fontSize(14)
.fontColor('#111111')
.backgroundColor(Color.White)
.borderRadius(0)
.border({ width: 1, color: '#111111' })
.onClick(() => {
this.clearAll();
})
}
.width('100%')
.margin({ top: 16 })
代码说明:
按钮组采用黑白对比风格:
- "写入存储"按钮:黑色实心背景、白色文字,
.borderRadius(0)设置为直角,体现极简风格。 - "清空全部"按钮:白色背景、黑色文字,带 1 像素黑色描边,形成描边按钮效果。
- 两个按钮通过
.layoutWeight(1)各占一半宽度。 .onClick回调分别调用saveRow和clearAll方法。
this.SectionTitle('技术要点')
// 要点列表(另一种"表格":编号圆点)
ForEach(['getPreferences 获取实例', 'put / get / flush 同步落盘', 'clear 清空全部数据', '支持 string / number / boolean'], (tip: string, i: number) => {
Row({ space: 10 }) {
Text(`${i + 1}`)
.fontSize(11)
.fontColor(Color.White)
.width(20)
.height(20)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor('#111111')
Text(tip)
.fontSize(13)
.fontColor('#333333')
}
.width('100%')
.padding({ top: 8, bottom: 8 })
})
}
.padding(20)
.width('100%')
}
.width('100%')
.backgroundColor('#FFFFFF')
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
}
}
代码说明:
技术要点部分使用编号圆点列表展示核心知识点:
ForEach遍历一个字符串数组,每个元素是一个技术要点。- 每个要点左侧是一个黑色圆形编号(20x20 的圆,
borderRadius(10)使其成为正圆),右侧是文字说明。 - 这种设计既是一种信息展示表格,又区别于传统的行列表格,体现了页面风格的多样性。
五、Preferences 完整 API 详解
5.1 获取实例
// 方式一:异步获取
const prefs = await preferences.getPreferences(context, 'myStore');
// 方式二:带选项获取
const options: preferences.Options = {
name: 'myStore',
dataGroupId: 'group1'
};
const prefs2 = await preferences.getPreferences(context, options);
代码说明:
getPreferences(context, name):根据名称获取实例,name 是文件名(不含后缀)。- 第二个参数也可以传入
Options对象,支持指定dataGroupId(数据组 ID),用于跨应用数据共享场景。 - 该方法返回 Promise,需要使用
await或.then()处理。
5.2 写入数据
// 写入字符串
await prefs.put('user_name', '张三');
// 写入数字
await prefs.put('login_count', 5);
// 写入布尔值
await prefs.put('is_vip', true);
// 强制落盘
await prefs.flush();
代码说明:
put(key, value):写入数据,value 支持 string、number、boolean 类型。- 多个
put操作可以合并后统一flush,减少磁盘 I/O 次数,提升性能。
5.3 读取数据
// 读取字符串,带默认值
const name = await prefs.get('user_name', '默认值');
// 读取数字,带默认值
const count = await prefs.get('login_count', 0);
// 读取布尔值,带默认值
const vip = await prefs.get('is_vip', false);
// 获取所有键值对
const all = await prefs.getAll();
代码说明:
get(key, defaultValue):读取指定键的值,如果键不存在则返回默认值。默认值参数是必须的。getAll():获取实例中所有键值对,返回一个对象。- 注意:
get返回值的类型是ValueType,即string | number | boolean的联合类型。如果需要特定类型,需要进行类型转换。
5.4 删除与清空
// 删除单个键
await prefs.delete('user_name');
// 清空所有数据
await prefs.clear();
// 删除整个实例(从磁盘删除文件)
await preferences.deletePreferences(context, 'myStore');
代码说明:
delete(key):删除指定键的数据。clear():清空实例中的所有数据,但实例本身仍存在。deletePreferences(context, name):从磁盘上彻底删除存储文件,此操作不可逆,使用前需谨慎。
5.5 数据变更监听
// 注册监听
const observer = (key: string) => {
console.info(`键 ${key} 的数据发生了变化`);
};
prefs.on('change', observer);
// 取消监听
prefs.off('change', observer);
代码说明:
Preferences 支持数据变更监听机制:
on('change', callback):当数据发生变化时触发回调,回调参数是发生变化的键名。off('change', callback):取消监听。- 监听机制非常适合多页面之间数据同步的场景,例如设置页修改了主题,首页可以监听变化并自动刷新。
六、底层原理剖析
6.1 存储文件结构
Preferences 的底层存储文件是一个 JSON 格式的文件,后缀为 .preferences。文件内容大致如下:
{
"user_name": "张三",
"theme_mode": "dark",
"login_count": 5
}
6.2 内存与磁盘的双层结构
Preferences 采用"内存为主、磁盘为辅"的设计:
- 内存缓存层:所有
put操作首先写入内存中的 Map 结构,读取操作也优先从内存读取,保证读写速度。 - 磁盘持久层:调用
flush()时,将内存中的数据序列化为 JSON 写入磁盘文件。 - 启动加载:应用启动获取实例时,会自动从磁盘加载数据到内存。
这种设计的优势是读多写少场景下性能极佳,但代价是如果应用在 flush 之前崩溃,内存中未落盘的数据会丢失。
6.3 为什么需要 flush
很多初学者会困惑:为什么 put 之后还要调用 flush?原因在于:
- 性能考虑:频繁的磁盘写入会严重影响性能,所以 Preferences 将写入操作缓存到内存。
- 批量写入:通过
flush可以将多次put操作合并为一次磁盘写入,大幅减少 I/O。 - 时机控制:开发者可以自主控制落盘时机,例如在应用进入后台时统一 flush。
七、最佳实践与性能优化
7.1 合理控制数据量
Preferences 适合存储少量数据(建议不超过 1KB),如果数据量较大,应该考虑使用关系型数据库(RelationalStore)或文件存储。
7.2 批量操作
// 不推荐:多次 flush
await prefs.put('a', 1);
await prefs.flush();
await prefs.put('b', 2);
await prefs.flush();
// 推荐:一次 flush
await prefs.put('a', 1);
await prefs.put('b', 2);
await prefs.flush();
7.3 键名规范
使用有意义且统一的键名,推荐使用"模块_语义"的命名方式:
await prefs.put('user_name', '张三');
await prefs.put('user_age', 18);
await prefs.put('setting_theme', 'dark');
7.4 异步处理
所有 Preferences API 都是异步的,务必使用 await 或 .then() 处理,避免出现未捕获的 Promise 异常。
八、常见问题与解决方案
8.1 数据写入后重启丢失
原因:调用 put 后没有调用 flush,数据只保存在内存中。
解决方案:
await prefs.put('key', 'value');
await prefs.flush(); // 必须调用
8.2 类型不匹配
原因:get 返回的是联合类型 ValueType,直接赋值给特定类型变量会报错。
解决方案:
const raw = await prefs.get('login_count', 0);
const count: number = raw as number; // 类型断言
8.3 实例获取失败
原因:传入的上下文类型不正确,或者文件名非法。
解决方案:
const ctx = getContext(this) as common.UIAbilityContext;
const prefs = await preferences.getPreferences(ctx, 'myStore');
九、总结
本文从实战角度完整讲解了 HarmonyOS Preferences 键值存储的使用方法。我们实现了一个功能完整的极简黑白风格页面,包含数据展示、写入、清空等完整功能,并深入剖析了 Preferences 的底层原理、完整 API、最佳实践和常见问题。
核心要点回顾:
- Preferences 是轻量级键值存储,适合存储配置类少量数据。
- 核心 API:
getPreferences、put、get、flush、clear。 - 写入后必须
flush才能持久化。 - 所有 API 均为异步,需用
await处理。 - 支持数据变更监听,便于多页面同步。
掌握 Preferences 是 HarmonyOS 开发的基础技能,它为后续学习关系型数据库、分布式数据库等更复杂的数据管理方案打下了坚实基础。希望本文对读者有所帮助,下一篇我们将深入讲解 HarmonyOS 网络请求的实战技巧。
更多推荐



所有评论(0)