Preferences 键值存储 --- 轻量级配置数据的持久化方案
一、引言
在应用开发中,大量的配置类数据(如用户偏好、开关状态、简单缓存)不需要复杂的数据库支持,只需要一个轻量级的键值存储方案。鸿蒙系统的 Preferences(首选项)正是为此类场景设计,它以简洁的 API、异步的操作和良好的性能,成为配置数据持久化的首选方案。MoneyTrack 在提醒设置、预算金额等场景中大量使用 Preferences,实现了配置数据的快速读写和持久化。
二、核心知识点
2.1 Preferences 完整使用步骤
Preferences 的使用遵循「获取实例 → 读写数据 → 刷入磁盘」的标准流程:
typescript
import { preferences } from '@kit.ArkData'
async function demoPreferences(context: Context) {
// 第一步:获取 Preferences 实例
// getPreferences 是异步操作,返回 Preferences 对象
// 文件名 'demo_pref' 对应磁盘上的一个 XML 文件
const pref = await preferences.getPreferences(context, 'demo_pref')
// 第二步:写入数据(修改在内存中,尚未落盘)
await pref.put('username', 'alice') // string
await pref.put('age', 25) // number
await pref.put('is_vip', true) // boolean
await pref.put('tags', ['premium', 'yearly']) // Array<string>
// 第三步:刷入磁盘(必须显式调用 flush)
// flush 将内存中的所有修改批量写入磁盘文件
await pref.flush()
// 读取数据(需指定默认值,防止首次读取返回 undefined)
const username = await pref.get('username', 'default_user')
const age = await pref.get('age', 18)
const isVip = await pref.get('is_vip', false)
}
关于 flush 的说明 :flush() 是异步批量写入操作,调用后不保证立即完成。在应用进入后台或即将退出前调用 flush() 可确保数据安全。
2.2 异步读写流程
磁盘 XML 内存缓存 Preferences API 应用层 磁盘 XML 内存缓存 Preferences API 应用层 #mermaid-svg-58ctUeKUcVaQBcP9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-58ctUeKUcVaQBcP9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-58ctUeKUcVaQBcP9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-58ctUeKUcVaQBcP9 .error-icon{fill:#552222;}#mermaid-svg-58ctUeKUcVaQBcP9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-58ctUeKUcVaQBcP9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-58ctUeKUcVaQBcP9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-58ctUeKUcVaQBcP9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-58ctUeKUcVaQBcP9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-58ctUeKUcVaQBcP9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-58ctUeKUcVaQBcP9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-58ctUeKUcVaQBcP9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-58ctUeKUcVaQBcP9 .marker.cross{stroke:#333333;}#mermaid-svg-58ctUeKUcVaQBcP9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-58ctUeKUcVaQBcP9 p{margin:0;}#mermaid-svg-58ctUeKUcVaQBcP9 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-58ctUeKUcVaQBcP9 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-58ctUeKUcVaQBcP9 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-58ctUeKUcVaQBcP9 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-58ctUeKUcVaQBcP9 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-58ctUeKUcVaQBcP9 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-58ctUeKUcVaQBcP9 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-58ctUeKUcVaQBcP9 .sequenceNumber{fill:white;}#mermaid-svg-58ctUeKUcVaQBcP9 #sequencenumber{fill:#333;}#mermaid-svg-58ctUeKUcVaQBcP9 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-58ctUeKUcVaQBcP9 .messageText{fill:#333;stroke:none;}#mermaid-svg-58ctUeKUcVaQBcP9 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-58ctUeKUcVaQBcP9 .labelText,#mermaid-svg-58ctUeKUcVaQBcP9 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-58ctUeKUcVaQBcP9 .loopText,#mermaid-svg-58ctUeKUcVaQBcP9 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-58ctUeKUcVaQBcP9 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-58ctUeKUcVaQBcP9 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-58ctUeKUcVaQBcP9 .noteText,#mermaid-svg-58ctUeKUcVaQBcP9 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-58ctUeKUcVaQBcP9 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-58ctUeKUcVaQBcP9 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-58ctUeKUcVaQBcP9 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-58ctUeKUcVaQBcP9 .actorPopupMenu{position:absolute;}#mermaid-svg-58ctUeKUcVaQBcP9 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-58ctUeKUcVaQBcP9 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-58ctUeKUcVaQBcP9 .actor-man circle,#mermaid-svg-58ctUeKUcVaQBcP9 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-58ctUeKUcVaQBcP9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 其他页面修改数据时 getPreferences(context, name) 读取 XML 文件 返回数据 加载到内存缓存 返回 Preferences 实例 put(key, value) 更新内存缓存 void get(key, default) 从内存读取 value value flush() 批量写入磁盘 写入完成 void on('change', callback) 注册监听 触发 change 回调
2.3 多实例隔离
Preferences 支持基于文件名创建多个独立实例,不同模块使用不同的文件,实现数据隔离:
typescript
// 不同模块使用独立的 Preferences 文件
const reminderPref = await preferences.getPreferences(context, 'reminder_settings')
const themePref = await preferences.getPreferences(context, 'theme_settings')
const budgetPref = await preferences.getPreferences(context, 'budget_settings')
// 每个文件独立存储,互不干扰
await reminderPref.put('enabled', true)
await themePref.put('theme', 'dark')
await budgetPref.put('monthly_limit', 5000)
多实例隔离的好处:
- 模块间数据解耦,修改一个模块的配置不影响其他模块
- 文件粒度的管理,方便单独清除某个模块的缓存
- 减少单个文件大小,提升读写性能
2.4 数据删除操作
除了读写,Preferences 还提供了数据删除能力:
typescript
// 删除指定的 key-value
await pref.delete('temp_setting')
// 检查某个 key 是否存在
const hasKey = await pref.has('reminder_enabled')
// 清空所有数据(谨慎使用)
await pref.clear()
// 删除 Preferences 文件(释放磁盘空间)
await preferences.deletePreferences(context, 'obsolete_settings')
// 注意:delete 和 clear 后都需要 flush 才能持久化
await pref.flush()
2.5 数据变更监听
Preferences 支持通过 on('change') 事件监听数据变更,这在多页面共享同一配置时特别有用------当某个页面修改了配置,其他页面可以实时感知变化并更新 UI:
typescript
preferences.on('change', (key) => {
if (key === 'reminder_enabled') {
this.refreshUI()
}
})
三、项目代码案例
3.1 ReminderSettingVM 完整实现
在 products/entry/src/main/ets/viewmodel/reminder/ReminderSettingVM.ets 中,ReminderSettingVM 封装了提醒设置的全部逻辑:
typescript
export class ReminderSettingVM {
private pref: preferences.Preferences | null = null
private context: Context
// 持久化键值
private static readonly KEY_ENABLED = 'reminder_enabled'
private static readonly KEY_TIME = 'reminder_time'
private static readonly KEY_PERIOD = 'reminder_period'
private static readonly PREF_NAME = 'reminder_settings'
constructor(context: Context) {
this.context = context
}
// 初始化:加载偏好设置
async init(): Promise<void> {
this.pref = await preferences.getPreferences(this.context, ReminderSettingVM.PREF_NAME)
// 注册监听
this.pref.on('change', (key) => {
console.log('提醒设置变更:', key)
})
}
// 读取提醒是否开启
async isEnabled(): Promise<boolean> {
return await this.pref!.get(ReminderSettingVM.KEY_ENABLED, false)
}
// 设置提醒开关
async setEnabled(enabled: boolean): Promise<void> {
await this.pref!.put(ReminderSettingVM.KEY_ENABLED, enabled)
await this.pref!.flush()
}
// 读取提醒时间
async getReminderTime(): Promise<string> {
return await this.pref!.get(ReminderSettingVM.KEY_TIME, '08:00')
}
// 设置提醒时间
async setReminderTime(time: string): Promise<void> {
await this.pref!.put(ReminderSettingVM.KEY_TIME, time)
await this.pref!.flush()
}
}
3.2 预算金额持久化
在预算管理模块中,月度预算金额通过 Preferences 持久化:
typescript
// 键:budget_{year}_{month},支持分别存储每个月的预算
async saveMonthlyBudget(year: number, month: number, amount: number) {
const pref = await preferences.getPreferences(this.context, 'budget_settings')
const key = `budget_${year}_${month}`
await pref.put(key, amount)
await pref.flush()
}
async loadMonthlyBudget(year: number, month: number): Promise<number> {
const pref = await preferences.getPreferences(this.context, 'budget_settings')
const key = `budget_${year}_${month}`
return await pref.get(key, 0)
}
3.3 使用注意事项
- 及时 flush :修改后调用
flush()确保数据落盘,避免应用退出导致数据丢失 - 默认值 :使用
get()方法时始终提供合理的默认值,处理首次使用场景 - 键名规范 :使用有意义的键名,如
module_key的命名模式 - 数据量控制:Preferences 适合少量配置数据(建议不超过 100 个 key),大量数据应使用 RDB
- 避免频繁 flush:多次 put 后统一 flush 一次,减少磁盘 I/O