HarmonyOS 5 数据持久化:首选项 (Preferences)
本文介绍了HarmonyOS 5中轻量级数据持久化方案——首选项(Preferences)的使用方法。首选项适合存储简单的键值对数据(如用户设置、登录token等),相比数据库更轻量高效。文章详细讲解了核心API的同步读写操作,并提供了封装工具类PreferenceUtil的实现方案,包括单例模式、初始化、数据存取等关键方法。特别强调了在EntryAbility中正确初始化的时机,并针对常见问题给
HarmonyOS 5 数据持久化:首选项 (Preferences)
大家好,我是不想掉发的鸿蒙开发工程师 城中的雾,上一系列了解了应用的 Context ,App 得有常见了还有数据存储这一期我们来聊一聊数据的持久化。想象一下:用户费劲登录了你的 App,结果杀掉进程再打开,又要登录?或者刚把主题设为深色,重启后数据是否要保留,我们来学习数据的常见持久化方式:用户首选项 (Preferences)。
1. 它是轻量级存储最常用
在鸿蒙中,存储数据的方式有很多,但如果你只是想存一些简单的 Key-Value(键值对) 数据,比如:
- 用户是否开启了推送?(Boolean)
- 用户的字号大小是 14 还是 16?(Number)
- 用户的登录 Token 是多少?(String)
那么,首选项 (Preferences) 是你的不二之选。
为什么不用数据库?
为了存个“字体大小”去建一张表、写 SQL 语句,既麻烦又浪费性能。首选项底层基于 XML 或轻量级文件,读写速度极快,加载内存占用极小。
为什么不用 AppStorage?
AppStorage 确实方便,但它默认存在内存里。App 一关,数据就没了(除非配合 PersistentStorage,这个我们下期讲)。而首选项是实打实写进磁盘文件的。
避坑预警:
首选项不适合存大量数据!官方建议每条记录不超过 8KB,总文件大小如果不必要不要太大。别试图往里面塞高清大图的 Base64,否则会导致 App 启动卡顿。
2. 核心 API
在 API 10+ 中,官方提供了同步接口(Sync),这使得首选项的读写变得非常简单,不再需要满屏的 await。
使用首选项通常分三步走:拿实例 -> 读/写 -> 刷盘。
import { preferences } from '@kit.ArkData';
// 1. 同步获取首选项实例 (需要 Context)
// 'my_store' 是文件名,会生成 my_store.xml
// 使用 getPreferencesSync 会阻塞当前线程直到读取完成,适合轻量级数据
let pref = preferences.getPreferencesSync(context, { name: 'my_store' });
// 2. 同步写入数据 (PutSync)
pref.putSync('is_night_mode', true);
// 3. 极其重要!持久化刷盘 (Flush)
// 注意:API 暂无 flushSync,flush 依然是异步的
// 但 putSync 已经更新了内存,后续 getSync 能立即读到,不影响业务
pref.flushSync();
// 4. 同步读取数据 (GetSync)
// 第二个参数是默认值,如果读不到就返回它
let value = pref.getSync('is_night_mode', false);
3. 封装一个 PreferenceUtil
利用同步接口,我们可以封装一个无需 await 的全局工具类,彻底解决异步初始化带来的“竞态问题”。
第一步:编写工具类 (PreferenceUtil.ets)
调试小技巧:我们在构造函数里加了日志。如果您在 Log 面板看到 "PreferenceUtil: 创建实例" 出现了两次,说明您遇到了多实例问题,请检查 import 路径。
// utils/PreferenceUtil.ets
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
export class PreferenceUtil {
// 单例模式
private static instance: PreferenceUtil;
// 持有首选项实例
private pref: preferences.Preferences | null = null;
// 定义文件名
private readonly PREF_NAME = 'my_app_store';
private constructor() {
// 【调试日志】如果这个日志打印了多次,说明单例失效(可能是多包引用导致)
console.info('PreferenceUtil: 创建实例 (Instance Created)');
}
public static get(): PreferenceUtil {
if (!PreferenceUtil.instance) {
PreferenceUtil.instance = new PreferenceUtil();
}
return PreferenceUtil.instance;
}
/**
* 初始化首选项 (同步方法)
* 建议在 EntryAbility 的 onCreate 中调用
* @param context ApplicationContext
*/
init(context: common.Context) {
// 增加重复初始化检查,避免覆盖
if (this.pref) {
console.info('PreferenceUtil: 已经初始化过,跳过');
return;
}
try {
// 使用同步方法,阻塞等待文件加载,保证后续 getSync 能立即取到数据
this.pref = preferences.getPreferencesSync(context, { name: this.PREF_NAME });
console.info('PreferenceUtil: 初始化成功');
} catch (err) {
console.error('PreferenceUtil: 初始化失败', JSON.stringify(err));
}
}
/**
* 保存数据 (同步更新内存,异步刷新磁盘)
* @param key 键
* @param value 值
*/
put(key: string, value: preferences.ValueType) {
if (!this.pref) {
console.error(`PreferenceUtil: [PUT FAILED] 实例未初始化! 请检查 import 路径或初始化流程。Key: ${key}`);
return;
}
try {
// 1. 同步写入内存 (内存操作极其迅速)
this.pref.putSync(key, value);
// 2. 异步刷盘 (持久化)
// 注意:鸿蒙暂无 flushSync 接口。
// 虽然这里是异步的,但 putSync 已保证内存一致性,后续的 getSync 可立即读到最新值。
// 我们添加 catch 防止潜在的 Promise 异常抛出。
this.pref.flushSync().catch((err: BusinessError) => {
console.error(`PreferenceUtil: 持久化刷盘失败 (Key: ${key})`, JSON.stringify(err));
});
} catch (err) {
console.error(`PreferenceUtil: 保存 ${key} 失败`, JSON.stringify(err));
}
}
/**
* 获取数据 (同步)
* @param key 键
* @param defaultValue 默认值
*/
get(key: string, defaultValue: preferences.ValueType): preferences.ValueType {
if (!this.pref) {
console.warn(`PreferenceUtil: [GET FAILED] 实例未初始化,返回默认值。Key: ${key}`);
return defaultValue;
}
try {
return this.pref.getSync(key, defaultValue);
} catch (err) {
console.error(`PreferenceUtil: 读取 ${key} 失败`, JSON.stringify(err));
return defaultValue;
}
}
/**
* 删除数据 (同步)
*/
delete(key: string) {
if (!this.pref) return;
try {
this.pref.deleteSync(key);
// 同样异步刷盘并处理错误
this.pref.flushSync().catch((err: BusinessError) => {
console.error(`PreferenceUtil: 删除刷盘失败 (Key: ${key})`, JSON.stringify(err));
});
} catch (err) {
console.error(`PreferenceUtil: 删除 ${key} 失败`, JSON.stringify(err));
}
}
}
第二步:在 EntryAbility 初始化
重点注意:根据实践反馈,建议在 loadContent 成功加载内容后进行初始化,确保环境准备就绪。
// EntryAbility.ets
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { PreferenceUtil } from '../utils/PreferenceUtil';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
// ...
}
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error('Failed to load the content.');
return;
}
// 在内容加载成功后初始化首选项
PreferenceUtil.get().init(this.context);
console.info('Succeeded in loading the content.');
});
}
}
4. 场景演练:数据管理控制台
为了更直观地展示增、删、查操作,我们编写一个简单的“控制台”页面。

UI 代码 (DataManagePage.ets):
import { PreferenceUtil } from '../utils/PreferenceUtil';
import { promptAction } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit'; // 导入 common 以使用 Context 类型
@Entry
@Component
struct DataManagePage {
@State inputText: string = ''; // 输入框内容
@State resultText: string = '等待读取...'; // 读取结果展示
private readonly KEY_TEST = 'my_test_key'; // 测试用的 Key
// 页面加载时执行
aboutToAppear() {
// 自动读取一次,验证初始化是否成功
this.refreshData();
}
// 封装读取逻辑
refreshData() {
const value = PreferenceUtil.get().get(this.KEY_TEST, '默认值(空)');
this.resultText = `当前存储值: ${value}`;
}
build() {
Column({ space: 20 }) {
Text('数据持久化控制台')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
// 1. 输入区域
TextInput({ placeholder: '请输入要保存的内容', text: this.inputText })
.onChange((val) => {
this.inputText = val;
})
.width('90%')
.padding(10)
.backgroundColor('#F5F5F5')
.borderRadius(8)
// 2. 操作按钮区域
Row({ space: 15 }) {
// 保存按钮
Button('保存数据')
.onClick(() => {
if (this.inputText === '') {
promptAction.showToast({ message: '内容不能为空' });
return;
}
// 同步保存,无需 await
PreferenceUtil.get().put(this.KEY_TEST, this.inputText);
promptAction.showToast({ message: '保存成功!' });
this.refreshData(); // 保存后立即刷新显示
})
.backgroundColor('#007DFF')
// 读取按钮
Button('读取数据')
.onClick(() => {
this.refreshData();
promptAction.showToast({ message: '读取完成' });
})
.backgroundColor('#00C15B')
// 删除按钮
Button('删除数据')
.onClick(() => {
// 同步删除
PreferenceUtil.get().delete(this.KEY_TEST);
this.inputText = ''; // 清空输入框
this.refreshData(); // 删除后刷新
promptAction.showToast({ message: '删除成功' });
})
.backgroundColor('#FF4040')
}
.width('100%')
.justifyContent(FlexAlign.Center)
// 3. 结果展示区域
Text(this.resultText)
.fontSize(16)
.fontColor('#666666')
.padding(20)
.border({ width: 1, color: '#E0E0E0', radius: 8 })
.width('90%')
.textAlign(TextAlign.Center)
}
.width('100%')
.height('100%')
.padding(20)
.justifyContent(FlexAlign.Start)
}
}
通过这个控制台 Demo,体验了数据的生命周期:
- 输入与保存:点击保存时,数据立即写入内存(同步)并触发后台刷盘(异步),界面无卡顿。
- 持久化验证:你可以尝试关闭应用(甚至强杀进程),再次打开时,
aboutToAppear中的refreshData依然能读到上次保存的内容。 - 代码体验:得益于
PreferenceUtil的封装,我们在 UI 层的代码中看不到任何await或Promise,逻辑清晰流畅,开发效率极高。
5. 总结与避坑指南
首选项虽然好用,但也有几个“雷区”千万别踩:
- 持久化是异步的:这是一个重要的知识点。虽然我们用
putSync实现了同步写内存。 - Key 命名冲突:首选项是 Key-Value 结构,建议使用命名空间前缀,比如
user_name、setting_font_size,避免不同模块把 Key 搞重复了。 - 不要存大文件:虽然
getPreferencesSync很爽,但它是阻塞主线程的。如果你往首选项里存了 1MB 的字符串,App 启动时就会黑屏卡顿!大文件请用 FileIO。
下一期预告:
首选项需要我们手动调用 get 和 put,有没有一种更高级的办法,像操作普通变量一样操作持久化数据?比如 AppStorage.set('token', '123') 就自动存盘?
当然有!
下一篇,我们将探讨 状态持久化 (PersistentStorage),看看它是如何将 UI 状态与磁盘绑定的,让你的变量持久化存储。
📚 充电时间
如果有想加入鸿蒙生态的大佬们,快来加入鸿蒙认证吧!初高级证书还没获取的,点这里:
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信中提出,非常感谢您的支持。
更多推荐



所有评论(0)