鸿蒙开发ArkData用户首选项实战:从夜间模式到多进程配置的完整案例
鸿蒙开发ArkData用户首选项实战:从夜间模式到多进程配置的完整案例
用户首选项(Preferences)是 ArkData 里最轻量的存储,但用得最频繁——几乎每个应用都要存配置。下面用一个"应用设置中心"的完整案例,把读写、订阅、存储模式选择、容易出问题的点全过一遍。
一、案例背景:一个设置中心
假设我们在做一个阅读应用,设置中心要持久化这些配置:
- 夜间模式开关(boolean)
- 字体大小(number,14-32)
- 阅读偏好(string,“翻页”/“滚动”)
- 自定义特殊字符(含非 UTF-8 字符的字符串)
这些都是典型的"轻量级、读多写少、需持久化"数据,正好是 Preferences 的主场。
二、第一步:导入模块和获取实例
import { preferences } from '@kit.ArkData';
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
let dataPreferences: preferences.Preferences | null = null;
class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage) {
// 获取应用上下文
const context = this.context;
// 配置选项,name 是持久化文件名
let options: preferences.Options = { name: 'myStore' };
// 同步获取 Preferences 实例
dataPreferences = preferences.getPreferencesSync(context, options);
}
}
几个要点:
name: 'myStore'是持久化文件名,每个文件唯一对应一个 Preferences 实例- 系统通过静态容器把实例存在内存里,直到主动移除或删除文件
- 持久化文件保存在应用沙箱内,路径通过 context 获取
三、第二步:选择存储模式(API 18 起)
Preferences 默认用 XML 格式存储。从 API 18 开始,多了 GSKV 模式。两种模式的差别很关键:
| 模式 | 格式 | 落盘时机 | 并发安全 | 跨平台 |
|---|---|---|---|---|
| XML(默认) | XML 文本 | 调用 flush 时 | 不安全 | 支持 |
| GSKV | 二进制 | 实时落盘 | 支持多进程并发 | 不支持 |
选型建议:单进程小数据量用 XML;多进程并发读写用 GSKV。
选 GSKV 前要先判断平台是否支持:
let isGskvSupported = preferences.isStorageTypeSupported(preferences.StorageType.GSKV);
if (isGskvSupported) {
let options: preferences.Options = {
name: 'myStore',
storageType: preferences.StorageType.GSKV
};
dataPreferences = preferences.getPreferencesSync(context, options);
}
注意:选定一种存储模式后不允许切换。所以这个决策要在最开始做对。
四、第三步:写入数据(含非 UTF-8 字符处理)
// 检查键是否存在,避免重复写入
if (dataPreferences.hasSync('nightMode')) {
console.info('nightMode 已存在');
} else {
// 写入基本类型
dataPreferences.putSync('nightMode', false);
dataPreferences.putSync('fontSize', 18);
dataPreferences.putSync('readMode', '翻页');
// 处理非 UTF-8 字符串:转成 Uint8Array 再存
let specialChars = '~!@#¥%……&*()——+?';
let uInt8Array = new util.TextEncoder().encodeInto(specialChars);
dataPreferences.putSync('specialChars', uInt8Array);
}
这里有个容易出问题的点:XML 模式下,字符串包含非 UTF-8 字符时,必须转成 Uint8Array 存储,否则持久化文件会格式错误、文件损坏。
写入后,XML 模式需要调用 flush() 才真正落盘;GSKV 模式实时落盘,不需要 flush。
五、第四步:读取数据
// 读取基本类型,第二个参数是默认值(键不存在时返回)
let nightMode = dataPreferences.getSync('nightMode', false) as boolean;
let fontSize = dataPreferences.getSync('fontSize', 18) as number;
let readMode = dataPreferences.getSync('readMode', '翻页') as string;
// 读取 Uint8Array 并转回字符串
let uInt8Array = dataPreferences.getSync('specialChars', new Uint8Array(0)) as Uint8Array;
let textDecoder = util.TextDecoder.create('utf-8');
let originalStr = textDecoder.decodeToString(uInt8Array);
getSync 的第二个参数是默认值——当键不存在或值为 null 时返回它。这个设计很实用,省去了先 hasSync 再 getSync 的两步操作。
六、第五步:订阅数据变更(设置页和阅读页联动)
设置中心和阅读页通常不在同一个组件,怎么让阅读页在用户改了字号后实时响应?用订阅。
// 订阅数据变更
let observer = (key: string) => {
console.info(`配置变更: ${key}`);
if (key === 'fontSize') {
// 读取新值并通知 UI 刷新
let newSize = dataPreferences.getSync('fontSize', 18) as number;
AppStorage.setOrCreate('currentFontSize', newSize);
}
};
dataPreferences.on('change', observer);
// 修改数据触发回调
dataPreferences.put('fontSize', 22, (err) => {
if (err) {
console.error(`写入失败: ${err.message}`);
return;
}
// XML 模式下,flush 后才会触发 observer
dataPreferences.flush((err) => {
if (err) {
console.error(`flush 失败: ${err.message}`);
return;
}
console.info('flush 成功,observer 已触发');
});
});
XML 模式和 GSKV 模式的回调时机不同:
- XML 模式:订阅的 key 变更后,执行 flush 时才触发 observer
- GSKV 模式:订阅的 key 变更后,立即触发 observer(因为实时落盘)
这个差异在跨模式迁移代码时最容易出 bug——从 XML 迁到 GSKV,回调时机变了,原来靠 flush 触发的逻辑可能提前执行。
七、第六步:删除数据
// 删除单个键值对
dataPreferences.deleteSync('specialChars');
删除后记得 flush(XML 模式)。
八、第七步:删除整个 Preferences 文件
let options: preferences.Options = { name: 'myStore' };
preferences.deletePreferences(context, options, (err) => {
if (err) {
console.error(`删除失败: ${err.message}`);
return;
}
console.info('删除成功');
});
deletePreferences 会从内存移除实例,同时删除持久化文件(包括备份文件、损坏文件)。三个注意点:
- 调用后不允许再使用该实例,否则数据一致性出问题
- 删除后数据不可恢复
- GSKV 模式下不支持与其他接口并发调用
九、约束限制速查
把官方的约束整理成速查表,避免出问题:
| 约束 | 说明 |
|---|---|
| Key 长度 | ≤ 1024 字节,非空 string |
| Value(string) | UTF-8 编码,≤ 16MB |
| 数据量建议 | ≤ 50MB,超过会变耗时操作 |
| 加密 | 不支持,需自行加密后用 Uint8Array 存 |
| XML 模式并发 | 不安全,多进程会文件损坏 |
| deletePreferences | 不允许与其他接口多线程/多进程并发 |
| 订阅生命周期 | removePreferencesFromCache 或 deletePreferences 后自动取消订阅 |
十、实战封装:一个 SettingsManager
把上面的散装 API 封装成一个可复用的管理类,实际项目里更好用:
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { util } from '@kit.ArkTS';
export class SettingsManager {
private static instance: SettingsManager | null = null;
private prefs: preferences.Preferences | null = null;
private constructor(context: common.Context) {
let options: preferences.Options = { name: 'app_settings' };
this.prefs = preferences.getPreferencesSync(context, options);
}
static getInstance(context: common.Context): SettingsManager {
if (!SettingsManager.instance) {
SettingsManager.instance = new SettingsManager(context);
}
return SettingsManager.instance;
}
setBoolean(key: string, value: boolean): void {
this.prefs?.putSync(key, value);
this.prefs?.flush();
}
getBoolean(key: string, defaultValue: boolean = false): boolean {
return this.prefs?.getSync(key, defaultValue) as boolean;
}
setNumber(key: string, value: number): void {
this.prefs?.putSync(key, value);
this.prefs?.flush();
}
getNumber(key: string, defaultValue: number = 0): number {
return this.prefs?.getSync(key, defaultValue) as number;
}
onChange(callback: (key: string) => void): void {
this.prefs?.on('change', callback);
}
offChange(callback: (key: string) => void): void {
this.prefs?.off('change', callback);
}
}
业务侧用起来就很干净:
let settings = SettingsManager.getInstance(this.context);
settings.setBoolean('nightMode', true);
let isNight = settings.getBoolean('nightMode');
settings.onChange((key) => {
if (key === 'nightMode') {
// 刷新夜间模式 UI
}
});
十一、几条经验
- 选对存储模式:单进程用 XML,多进程并发用 GSKV(API 18+),选定不可切换
- 非 UTF-8 字符串转 Uint8Array:XML 模式下不转会损坏文件
- XML 模式记得 flush:put 后不 flush 不落盘,订阅回调也不触发
- 数据量控制在 50MB 内:超过会变耗时操作,别在主线程跑
- 加密要自己做:Preferences 不支持加密,敏感数据先加密再存 Uint8Array
下一篇进入键值型数据库,处理数据量更大、需要分布式同步的场景。
更多推荐


所有评论(0)