HarmonyOS APP开发中的系统偏好——从读写到迁移,一篇搞定偏好设置
HarmonyOS APP开发中的系统偏好:@ohos.data.preferences 全解析——从读写到迁移,一篇搞定偏好设置
📌 核心要点:Preferences 是 HarmonyOS 轻量级键值存储方案,适合保存用户偏好、应用配置等小体量数据,支持变更监听与数据迁移
一、背景与动机
你有没有这样的经历——每次打开一个 App,它都"失忆"了:深色模式没了、字体大小重置了、上次浏览的位置也忘了。这种感觉就像你每天去同一家咖啡店,但店员从来不记得你爱喝美式。
这就是**偏好设置(Preferences)**要解决的问题。它就像一个贴心的小本子,帮你记住用户的选择和习惯。
在 HarmonyOS 中,@ohos.data.preferences 提供了轻量级的键值对存储能力。它不像关系型数据库那样"重",也不像全局变量那样"轻飘飘"——数据持久化到本地文件,应用重启后依然在。你可以把它理解为 Android 的 SharedPreferences,但能力更强、设计更优雅。
典型使用场景:
- 用户个性化设置(主题、语言、字体大小)
- 应用配置参数(是否首次启动、引导页状态)
- 功能开关(通知开关、自动更新开关)
- 简单的缓存数据(上次搜索关键词、最近使用列表)
二、核心原理
2.1 Preferences 架构概览
Preferences 本质上是一个基于文件系统的轻量级存储引擎。每个 Preferences 实例对应一个 XML 格式的本地文件,数据以键值对的形式存储在内存中,通过 flush() 操作将内存数据持久化到磁盘。
flowchart TB
A[应用层] -->|getPreferences| B[Preferences 实例]
B -->|内存缓存| C[键值对 Map]
C -->|flush 持久化| D[本地 XML 文件]
D -->|加载恢复| C
B -->|on\change 监听| E[变更回调]
E -->|通知| F[UI 刷新]
classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
classDef error fill:#F44336,stroke:#D32F2F,color:#fff
classDef info fill:#2196F3,stroke:#1976D2,color:#fff
classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff
class A primary
class B info
class C warning
class D error
class E purple
class F primary
2.2 数据存储模型
Preferences 支持以下数据类型:
| 类型 | 说明 | 示例 |
|---|---|---|
| number | 数值类型 | putSync('fontSize', 16) |
| string | 字符串类型 | putSync('theme', 'dark') |
| boolean | 布尔类型 | putSync('notify', true) |
| Array<number> | 数值数组 | putSync('recentIds', [1,2,3]) |
| Array<string> | 字符串数组 | putSync('tags', ['tech','life']) |
| Array<boolean> | 布尔数组 | putSync('flags', [true,false]) |
2.3 读写流程
sequenceDiagram
participant App as 应用代码
participant Prefs as Preferences
participant Mem as 内存缓存
participant Disk as 磁盘文件
App->>Prefs: getPreferences(context, name)
Prefs->>Disk: 读取XML文件
Disk-->>Prefs: 返回数据
Prefs->>Mem: 加载到内存
Prefs-->>App: 返回实例
App->>Prefs: put('key', value)
Prefs->>Mem: 更新内存缓存
App->>Prefs: flush()
Prefs->>Disk: 写入XML文件
Disk-->>Prefs: 写入成功回调
classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
classDef info fill:#2196F3,stroke:#1976D2,color:#fff
classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
classDef error fill:#F44336,stroke:#D32F2F,color:#fff
classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff
关键点在于:put 操作只修改内存,flush 才真正写入磁盘。这和你在 Word 里打字一样——输入内容只是暂存在内存,点"保存"才真正落盘。
三、代码实战
3.1 基础读写:封装 Preferences 管理器
这是最核心的用法——创建实例、读写数据、持久化存储。
import { preferences } from '@kit.ArkData';
import { Context } from '@kit.AbilityKit';
/**
* 偏好设置管理器
* 封装 Preferences 的初始化、读写、监听等操作
*/
class PreferencesManager {
private static instance: PreferencesManager;
private prefs: preferences.Preferences | null = null;
private readonly STORE_NAME = 'app_settings';
// 私有构造,单例模式
private constructor() {}
// 获取单例实例
static getInstance(): PreferencesManager {
if (!PreferencesManager.instance) {
PreferencesManager.instance = new PreferencesManager();
}
return PreferencesManager.instance;
}
/**
* 初始化偏好设置
* @param context 应用上下文
*/
async init(context: Context): Promise<void> {
try {
this.prefs = await preferences.getPreferences(context, this.STORE_NAME);
console.info('[Preferences] 初始化成功');
} catch (err) {
console.error(`[Preferences] 初始化失败: ${JSON.stringify(err)}`);
}
}
/**
* 写入字符串值
* @param key 键名
* @param value 值
*/
async putString(key: string, value: string): Promise<void> {
if (!this.prefs) {
console.error('[Preferences] 未初始化');
return;
}
try {
await this.prefs.put(key, value);
await this.prefs.flush(); // 立即持久化
console.info(`[Preferences] 写入成功: ${key} = ${value}`);
} catch (err) {
console.error(`[Preferences] 写入失败: ${JSON.stringify(err)}`);
}
}
/**
* 读取字符串值
* @param key 键名
* @param defaultValue 默认值
*/
getString(key: string, defaultValue: string = ''): string {
if (!this.prefs) {
console.error('[Preferences] 未初始化');
return defaultValue;
}
try {
return this.prefs.getStringSync(key, defaultValue);
} catch (err) {
console.error(`[Preferences] 读取失败: ${JSON.stringify(err)}`);
return defaultValue;
}
}
/**
* 写入数值
*/
async putNumber(key: string, value: number): Promise<void> {
if (!this.prefs) return;
try {
await this.prefs.put(key, value);
await this.prefs.flush();
} catch (err) {
console.error(`[Preferences] 写入数值失败: ${JSON.stringify(err)}`);
}
}
/**
* 读取数值
*/
getNumber(key: string, defaultValue: number = 0): number {
if (!this.prefs) return defaultValue;
try {
return this.prefs.getNumberSync(key, defaultValue);
} catch (err) {
return defaultValue;
}
}
/**
* 写入布尔值
*/
async putBoolean(key: string, value: boolean): Promise<void> {
if (!this.prefs) return;
try {
await this.prefs.put(key, value);
await this.prefs.flush();
} catch (err) {
console.error(`[Preferences] 写入布尔值失败: ${JSON.stringify(err)}`);
}
}
/**
* 读取布尔值
*/
getBoolean(key: string, defaultValue: boolean = false): boolean {
if (!this.prefs) return defaultValue;
try {
return this.prefs.getBooleanSync(key, defaultValue);
} catch (err) {
return defaultValue;
}
}
/**
* 检查键是否存在
*/
has(key: string): boolean {
if (!this.prefs) return false;
return this.prefs.hasSync(key);
}
/**
* 删除指定键
*/
async remove(key: string): Promise<void> {
if (!this.prefs) return;
try {
await this.prefs.delete(key);
await this.prefs.flush();
console.info(`[Preferences] 删除成功: ${key}`);
} catch (err) {
console.error(`[Preferences] 删除失败: ${JSON.stringify(err)}`);
}
}
/**
* 清空所有数据
*/
async clear(): Promise<void> {
if (!this.prefs) return;
try {
await this.prefs.clear();
await this.prefs.flush();
console.info('[Preferences] 清空成功');
} catch (err) {
console.error(`[Preferences] 清空失败: ${JSON.stringify(err)}`);
}
}
}
// 导出单例
export default PreferencesManager;
3.2 偏好变更监听:实时响应设置变化
当用户在设置页面修改了主题或字体大小,主页面需要立刻响应。这时候变更监听就派上用场了。
import { preferences } from '@kit.ArkData';
import { Context } from '@kit.AbilityKit';
import { emitter } from '@kit.BasicServicesKit';
/**
* 偏好设置监听器
* 监听偏好数据变化,通过 emitter 通知 UI 层刷新
*/
class PreferenceObserver {
private prefs: preferences.Preferences | null = null;
// 自定义事件ID,用于 emitter 通知
static readonly EVENT_THEME_CHANGED = 10001;
static readonly EVENT_FONT_CHANGED = 10002;
static readonly EVENT_NOTIFY_CHANGED = 10003;
/**
* 初始化并注册监听
*/
async init(context: Context): Promise<void> {
this.prefs = await preferences.getPreferences(context, 'app_settings');
// 注册偏好变更监听
this.prefs.on('change', (data: preferences.PreferenceChangeData) => {
console.info(`[Observer] 偏好变更: key = ${data.key}`);
this.dispatchChange(data.key!);
});
}
/**
* 根据变更的 key 分发事件
*/
private dispatchChange(key: string): void {
const keyMap: Record<string, number> = {
'app_theme': PreferenceObserver.EVENT_THEME_CHANGED,
'font_size': PreferenceObserver.EVENT_FONT_CHANGED,
'notify_enabled': PreferenceObserver.EVENT_NOTIFY_CHANGED,
};
const eventId = keyMap[key];
if (eventId) {
// 通过 emitter 发送事件,通知 UI 刷新
emitter.emit({ eventId });
console.info(`[Observer] 分发事件: eventId = ${eventId}`);
}
}
/**
* 移除监听
*/
removeListener(): void {
if (this.prefs) {
this.prefs.off('change');
console.info('[Observer] 监听已移除');
}
}
}
/**
* 设置页面:展示变更监听在 UI 中的使用
*/
@Entry
@Component
struct SettingsPage {
@State currentTheme: string = 'light';
@State fontSize: number = 16;
private prefs: preferences.Preferences | null = null;
async aboutToAppear(): Promise<void> {
const context = getContext(this);
this.prefs = await preferences.getPreferences(context, 'app_settings');
// 读取当前值
this.currentTheme = this.prefs.getStringSync('app_theme', 'light');
this.fontSize = this.prefs.getNumberSync('font_size', 16);
// 注册 emitter 监听,接收偏好变更通知
emitter.on({ eventId: PreferenceObserver.EVENT_THEME_CHANGED }, () => {
if (this.prefs) {
this.currentTheme = this.prefs.getStringSync('app_theme', 'light');
}
});
emitter.on({ eventId: PreferenceObserver.EVENT_FONT_CHANGED }, () => {
if (this.prefs) {
this.fontSize = this.prefs.getNumberSync('font_size', 16);
}
});
}
aboutToDisappear(): void {
emitter.off(PreferenceObserver.EVENT_THEME_CHANGED);
emitter.off(PreferenceObserver.EVENT_FONT_CHANGED);
}
build() {
Column() {
Text('应用设置')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
// 主题切换
Row() {
Text('当前主题')
.fontSize(this.fontSize)
.layoutWeight(1)
Text(this.currentTheme === 'dark' ? '🌙 深色' : '☀️ 浅色')
.fontSize(this.fontSize)
.fontColor('#4CAF50')
}
.width('100%')
.padding(16)
.backgroundColor(this.currentTheme === 'dark' ? '#333' : '#f5f5f5')
.borderRadius(12)
.onClick(async () => {
const newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
await this.prefs?.put('app_theme', newTheme);
await this.prefs?.flush();
})
// 字体大小调节
Row() {
Text('字体大小')
.fontSize(this.fontSize)
.layoutWeight(1)
Text(`${this.fontSize}`)
.fontSize(this.fontSize)
.fontColor('#FF9800')
}
.width('100%')
.padding(16)
.margin({ top: 12 })
.backgroundColor('#f5f5f5')
.borderRadius(12)
.onClick(async () => {
const newSize = this.fontSize >= 24 ? 12 : this.fontSize + 2;
await this.prefs?.put('font_size', newSize);
await this.prefs?.flush();
})
}
.width('100%')
.height('100%')
.padding(20)
}
}
3.3 偏好数据迁移:应用升级时的数据搬家
应用从 v1 升级到 v2 时,偏好数据的 key 结构可能发生变化。比如 v1 用 theme,v2 改成了 app_theme。这时候就需要数据迁移。
import { preferences } from '@kit.ArkData';
import { Context } from '@kit.AbilityKit';
/**
* 偏好数据迁移工具
* 处理应用版本升级时的偏好数据迁移
*/
class PreferenceMigrator {
private context: Context;
constructor(context: Context) {
this.context = context;
}
/**
* 执行数据迁移
* @param fromVersion 旧版本号
* @param toVersion 新版本号
*/
async migrate(fromVersion: number, toVersion: number): Promise<void> {
console.info(`[Migrator] 开始迁移: v${fromVersion} → v${toVersion}`);
const prefs = await preferences.getPreferences(this.context, 'app_settings');
const currentVersion = prefs.getNumberSync('version', 1);
// 逐版本递进迁移
let version = currentVersion;
while (version < toVersion) {
const nextVersion = version + 1;
await this.migrateToVersion(prefs, version, nextVersion);
version = nextVersion;
}
// 更新版本号
await prefs.put('version', toVersion);
await prefs.flush();
console.info(`[Migrator] 迁移完成: v${toVersion}`);
}
/**
* 单版本迁移逻辑
*/
private async migrateToVersion(
prefs: preferences.Preferences,
fromV: number,
toV: number
): Promise<void> {
switch (toV) {
case 2:
// v1 → v2: 重命名 theme → app_theme
await this.renameKey(prefs, 'theme', 'app_theme');
// 新增默认值
if (!prefs.hasSync('notify_enabled')) {
await prefs.put('notify_enabled', true);
}
console.info('[Migrator] v1 → v2 迁移完成');
break;
case 3:
// v2 → v3: font_size 从字符串改为数值
const fontSizeStr = prefs.getStringSync('font_size', '');
if (fontSizeStr) {
const fontSizeNum = parseInt(fontSizeStr, 10);
if (!isNaN(fontSizeNum)) {
await prefs.put('font_size', fontSizeNum);
}
}
// 新增语言设置
if (!prefs.hasSync('app_language')) {
await prefs.put('app_language', 'zh-CN');
}
console.info('[Migrator] v2 → v3 迁移完成');
break;
default:
console.warn(`[Migrator] 未知版本迁移: v${fromV} → v${toV}`);
}
}
/**
* 重命名键
*/
private async renameKey(
prefs: preferences.Preferences,
oldKey: string,
newKey: string
): Promise<void> {
if (prefs.hasSync(oldKey)) {
const value = prefs.getStringSync(oldKey, '');
await prefs.put(newKey, value);
await prefs.delete(oldKey);
console.info(`[Migrator] 重命名: ${oldKey} → ${newKey}`);
}
}
/**
* 从另一个偏好文件导入数据
* 适用于应用合并或数据整合场景
*/
async importFrom(sourceStoreName: string, keyMapping: Record<string, string>): Promise<void> {
const targetPrefs = await preferences.getPreferences(this.context, 'app_settings');
const sourcePrefs = await preferences.getPreferences(this.context, sourceStoreName);
for (const [sourceKey, targetKey] of Object.entries(keyMapping)) {
if (sourcePrefs.hasSync(sourceKey)) {
const value = sourcePrefs.getStringSync(sourceKey, '');
await targetPrefs.put(targetKey, value);
console.info(`[Migrator] 导入: ${sourceKey} → ${targetKey} = ${value}`);
}
}
await targetPrefs.flush();
console.info('[Migrator] 数据导入完成');
}
}
/**
* 在 EntryAbility 中使用迁移
*/
class EntryAbility {
async onCreate(): Promise<void> {
const context = getContext(this);
const migrator = new PreferenceMigrator(context);
// 读取当前存储的版本号
const prefs = await preferences.getPreferences(context, 'app_settings');
const savedVersion = prefs.getNumberSync('version', 1);
const appVersion = 3; // 当前应用版本
if (savedVersion < appVersion) {
await migrator.migrate(savedVersion, appVersion);
}
}
}
export { PreferenceMigrator };
四、踩坑与注意事项
4.1 flush 时机——别忘,也别太勤
坑:只 put 不 flush,应用崩溃后数据丢失。
解:每次 put 后调用 flush()。但不要在循环里频繁 flush——它是异步 IO 操作,太频繁会影响性能。建议批量写入后统一 flush 一次。
// ❌ 错误:循环中频繁 flush
for (const [key, value] of Object.entries(settings)) {
await prefs.put(key, value);
await prefs.flush(); // 每次都刷盘,性能差
}
// ✅ 正确:批量写入后统一 flush
for (const [key, value] of Object.entries(settings)) {
await prefs.put(key, value);
}
await prefs.flush(); // 一次刷盘搞定
4.2 数据大小限制——Preferences 不是数据库
坑:把大量数据塞进 Preferences,导致读写变慢甚至 OOM。
解:Preferences 的建议数据量不超过 几百条键值对。大数据请用关系型数据库 @ohos.data.relationalStore 或分布式数据服务。
4.3 同步方法 vs 异步方法
坑:在 UI 主线程调用 getPreferencesSync 等同步方法,可能造成卡顿。
解:优先使用异步方法(getPreferences、put、flush),同步方法(getStringSync 等)仅在确认数据量小且非频繁调用时使用。
4.4 多实例冲突
坑:同一文件创建多个 Preferences 实例,导致数据不一致。
解:同一文件只创建一个实例,通过单例模式管理。如果确实需要多实例访问同一文件,务必在写入后及时 flush。
4.5 on(‘change’) 监听的数据类型
坑:PreferenceChangeData 的 value 字段在 API 12+ 中已标记废弃,应通过 key 重新读取。
解:
prefs.on('change', (data: preferences.PreferenceChangeData) => {
// ❌ 旧方式(已废弃):直接用 data.value
// ✅ 新方式:通过 key 重新读取
const newValue = prefs.getStringSync(data.key!, '');
console.info(`变更: ${data.key} = ${newValue}`);
});
五、HarmonyOS 6 适配
5.1 API 变化一览
| 变化项 | HarmonyOS 5 | HarmonyOS 6 |
|---|---|---|
| 实例创建 | getPreferences() |
新增 getPreferencesSync() 但不推荐主线程使用 |
| 数据类型 | 支持 6 种基本类型 | 新增 bigint 类型支持 |
| 变更监听 | on('change') |
on('change') 回调参数简化,value 字段废弃 |
| 加密存储 | 不支持 | 新增 options.encryptionKey 参数,支持加密偏好文件 |
| 沙箱路径 | /data/app/el2/100/preferences/ |
路径不变,但新增 el5 加密级目录支持 |
5.2 迁移指南
// HarmonyOS 5 写法
const prefs = await preferences.getPreferences(context, 'myStore');
// HarmonyOS 6 写法(新增加密支持)
const prefs = await preferences.getPreferences(context, 'myStore', {
encryptionKey: 'your-32-byte-encryption-key-here' // 新增加密密钥
});
5.3 注意事项
- 加密偏好文件:HarmonyOS 6 支持对偏好文件设置加密密钥,但一旦设置,后续打开也必须传入相同密钥,否则无法读取数据。
- bigint 类型:新增的
bigint类型支持大整数存储,但读取时需使用getBigIntSync()方法。 - 废弃 API:
PreferenceChangeData.value已废弃,迁移时需改为通过 key 重新读取。
六、总结
mindmap
root((Preferences))
核心能力
键值对存储
6种数据类型
异步/同步双模式
读写操作
put / get
flush 持久化
has / delete / clear
变更监听
on\('change'\)
emitter 事件分发
UI 实时刷新
数据迁移
版本递进迁移
键重命名
跨文件导入
注意事项
flush 时机
数据量限制
多实例冲突
同步方法慎用
HarmonyOS 6
加密存储
bigint 类型
废弃 value 字段
| 知识点 | 要点 |
|---|---|
| 存储定位 | 轻量级键值存储,适合用户偏好和应用配置,不适合大数据 |
| 核心流程 | getPreferences → put → flush,三步完成持久化 |
| 变更监听 | on('change') 监听变化,结合 emitter 通知 UI 刷新 |
| 数据迁移 | 逐版本递进迁移,避免跳版本导致数据丢失 |
| flush 规则 | 批量写入后统一 flush,避免频繁 IO |
| HarmonyOS 6 | 新增加密存储和 bigint 类型,注意废弃 API 迁移 |
Preferences 就像是应用的"记忆中枢"——轻量、高效、用起来顺手。掌握了它的读写、监听和迁移,你的应用就再也不会"失忆"了。
- 点赞
- 收藏
- 关注作者
评论(0)