OpenHarmony 轻量键值存储 Preferences 完整封装(API Version23)
摘要
Preferences 是鸿蒙轻量本地持久化存储,适合存储简单键值配置(登录 Token、主题开关、用户基础配置、页面缓存参数),区别于 RDB 关系数据库:仅支持字符串、数字、布尔、数组基础类型,无需建表,读写速度更快。API23 重构 Preferences 实例生命周期、多实例隔离、同步 / 异步读写、数据自动落盘逻辑,修复旧版多页面并发读写错乱、修改后重启丢失、实例未关闭占用文件、批量写入卡顿等问题。本文封装全局 Preferences 工具类,封装布尔、字符串、数字、数组存取、删除 key、清空全部数据全套方法,搭配登录缓存、深色模式两套实战页面,给出实例销毁、并发读写、持久化同步规范,是项目基础存储必备底层工具。
关键词
OpenHarmony;ArkTS;API23;Preferences;轻量存储;键值持久化;本地配置缓存
一、引言
1.1 Preferences 适用场景区分
- Preferences(优先):简单配置、开关、token、账号缓存、主题状态,数据量小、读写频繁;
- RDB:结构化多条业务数据(笔记、通讯录、商品列表),支持分页、条件查询;
- fs 文件:图片、日志、大文本二进制文件;
- AppStorage:运行时全局内存状态,重启丢失,需搭配 Preferences 持久化。
API23 Preferences 核心升级变更:
- 强制每个存储文件独立实例,禁止全局单例长期占用;
- 新增自动 flush 落盘机制,修改后实时写入本地,防止重启数据丢失;
- 优化并发读写锁,多页面同时存取不会出现数据覆盖错乱;
- 区分同步 get/set 与异步 getAsync/setAsync 两套 API,主线程推荐异步避免 UI 卡顿;
- 废弃不关闭 getPreferences 实例写法,页面销毁必须 release 释放文件句柄;
- 支持基础数组类型直接存取,无需手动 JSON 序列化转换。
1.2 模块与基础初始化
依赖模块:@ohos.data.preferences无需动态权限,应用沙盒私有存储自动拥有读写权限 基础概念:
- 存储文件名:区分不同业务配置,如
user_config、app_setting - Preferences 实例:操作文件的句柄,用完必须 release 释放
二、API23 Preferences 原生基础示例
ets
import preferences from '@ohos.data.preferences' import common from '@ohos.app.ability.common' async function simplePrefDemo(ctx: common.UIAbilityContext) { // 获取配置文件实例 let pref = await preferences.getPreferences(ctx, "user_config") // 写入布尔值 await pref.putBool("is_login", true) // 强制落地本地文件 await pref.flush() // 读取布尔值,默认false const login = await pref.getBool("is_login", false) // 释放实例 await pref.release() }三、全局 Preferences 工具封装 utils/preference_util.ets
ets
import preferences from '@ohos.data.preferences' import common from '@ohos.app.ability.common' import promptAction from '@ohos.promptAction' // 配置文件常量,拆分业务存储文件,避免单文件过大 export const PREF_FILE = { USER: "user_config", // 用户登录、token、账号 SETTING: "app_setting" // 主题、系统开关配置 } class PrefUtil { private static instance: PrefUtil private ctx: common.UIAbilityContext | null = null // 缓存已创建的pref实例,避免重复创建 private prefMap: Map<string, preferences.Preferences> = new Map() static getInstance(): PrefUtil { if (!PrefUtil.instance) { PrefUtil.instance = new PrefUtil() } return PrefUtil.instance } // 注入页面/Ability上下文 setContext(context: common.UIAbilityContext) { this.ctx = context } // 根据文件名获取pref实例,复用缓存 private async getPref(fileName: string): Promise<preferences.Preferences | null> { if (!this.ctx) { promptAction.showToast({ message: "存储上下文未初始化" }) return null } if (this.prefMap.has(fileName)) { return this.prefMap.get(fileName)! } const pref = await preferences.getPreferences(this.ctx, fileName) this.prefMap.set(fileName, pref) return pref } // 写入布尔值 async putBool(file: string, key: string, value: boolean): Promise<boolean> { const pref = await this.getPref(file) if (!pref) return false try { await pref.putBool(key, value) await pref.flush() return true } catch (err) { console.error("putBool error", err) return false } } // 读取布尔值,def默认值 async getBool(file: string, key: string, def: boolean): Promise<boolean> { const pref = await this.getPref(file) if (!pref) return def return await pref.getBool(key, def) } // 写入字符串 async putString(file: string, key: string, value: string): Promise<boolean> { const pref = await this.getPref(file) if (!pref) return false await pref.putString(key, value) await pref.flush() return true } // 读取字符串 async getString(file: string, key: string, def: string): Promise<string> { const pref = await this.getPref(file) if (!pref) return def return await pref.getString(key, def) } // 写入数字 async putNumber(file: string, key: string, value: number): Promise<boolean> { const pref = await this.getPref(file) if (!pref) return false await pref.putNumber(key, value) await pref.flush() return true } // 读取数字 async getNumber(file: string, key: string, def: number): Promise<number> { const pref = await this.getPref(file) if (!pref) return def return await pref.getNumber(key, def) } // 写入字符串数组 async putStringArr(file: string, key: string, arr: string[]): Promise<boolean> { const pref = await this.getPref(file) if (!pref) return false await pref.putStringArray(key, arr) await pref.flush() return true } // 读取字符串数组 async getStringArr(file: string, key: string, def: string[]): Promise<string[]> { const pref = await this.getPref(file) if (!pref) return def return await pref.getStringArray(key, def) } // 删除单个key async deleteKey(file: string, key: string): Promise<boolean> { const pref = await this.getPref(file) if (!pref) return false await pref.delete(key) await pref.flush() return true } // 清空当前文件全部键值 async clearFile(file: string): Promise<boolean> { const pref = await this.getPref(file) if (!pref) return false await pref.clear() await pref.flush() return true } // 释放全部缓存实例(页面/应用退出调用) async releaseAll() { for (let [_, pref] of this.prefMap) { await pref.release() } this.prefMap.clear() } } export default PrefUtil.getInstance()四、两大业务实战页面
4.1 实战一:设置页(深色模式持久化)
ets
import PrefUtil, { PREF_FILE } from '../utils/preference_util' import { STORAGE_KEY } from '../model/Constant' @Entry @Component struct SettingPage { @State darkMode: boolean = false async aboutToAppear() { // 绑定页面上下文 const ctx = getContext(this) PrefUtil.setContext(ctx) // 读取本地持久化深色模式配置 this.darkMode = await PrefUtil.getBool(PREF_FILE.SETTING, STORAGE_KEY.DARK_MODE, false) } // 切换深色模式,同步写入本地存储 async toggleDark(val: boolean) { this.darkMode = val await PrefUtil.putBool(PREF_FILE.SETTING, STORAGE_KEY.DARK_MODE, val) // 同步更新全局AppStorage状态,所有页面UI自动刷新 AppStorage.Set(STORAGE_KEY.DARK_MODE, val) } build() { Column({ space: 25 }) { Text("系统设置(Preferences持久化演示)") .fontSize(22) .fontWeight(FontWeight.Bold) Row() { Text("开启深色模式").layoutWeight(1).fontSize(18) Toggle({ isOn: this.darkMode }) .onChange((val: boolean) => this.toggleDark(val)) } .width("90%") .padding(16) .backgroundColor(Color.White) .borderRadius(12) } .width("100%") .height("100%") .padding(20) .backgroundColor(this.darkMode ? "#181818" : "#f5f5f5") } async aboutToDisappear() { // 页面退出不释放全局实例,应用退出统一释放 } }4.2 实战二:登录页面(账号、Token 本地缓存)
ets
import PrefUtil, { PREF_FILE } from '../utils/preference_util' import HttpUtil from '../utils/http_util' import RouterUtil from '../utils/router_util' import { STORAGE_KEY, PAGE_ROUTE } from '../model/Constant' import AppStorage from '@ohos.arkui.state.AppStorage' import promptAction from '@ohos.promptAction' @Entry @Component struct LoginPage { @State account: string = "" @State password: string = "" async aboutToAppear() { const ctx = getContext(this) PrefUtil.setContext(ctx) // 读取上次登录账号回填输入框 this.account = await PrefUtil.getString(PREF_FILE.USER, "last_account", "") } async handleLogin() { if (!this.account || !this.password) { promptAction.showToast({ message: "账号密码不能为空" }) return } // 调用登录接口 const res = await HttpUtil.post<{ token: string }>("/login", { username: this.account, password: this.password }) if (!res) return // 持久化存储token、账号 await PrefUtil.putString(PREF_FILE.USER, "last_account", this.account) await PrefUtil.putString(PREF_FILE.USER, STORAGE_KEY.TOKEN, res.data.token) // 更新全局登录状态 AppStorage.Set(STORAGE_KEY.IS_LOGIN, true) AppStorage.Set(STORAGE_KEY.TOKEN, res.data.token) promptAction.showToast({ message: "登录成功" }) RouterUtil.replace(PAGE_ROUTE.INDEX) } // 退出登录清空用户存储 async logout() { await PrefUtil.clearFile(PREF_FILE.USER) AppStorage.Set(STORAGE_KEY.IS_LOGIN, false) AppStorage.Set(STORAGE_KEY.TOKEN, "") RouterUtil.replace(PAGE_ROUTE.LOGIN) } build() { Column({ space: 22 }) { Text("用户登录-账号本地缓存").fontSize(24).margin({ top: 60 }) TextInput({ text: this.account, placeholder: "请输入账号" }) .width("90%") .height(48) .onChange((val: string) => this.account = val) TextInput({ text: this.password, placeholder: "请输入密码" }) .width("90%") .height(48) .type(InputType.Password) .onChange((val: string) => this.password = val) Button("登录") .width("90%") .height(48) .backgroundColor("#007DFF") .onClick(() => this.handleLogin()) Button("清空登录缓存(退出登录)") .width("90%") .height(48) .backgroundColor("#f56c6c") .onClick(() => this.logout()) } .width("100%") .height("100%") .padding(20) } }五、API23 Preferences 编码与性能规范
5.1 文件拆分规范
- 按业务拆分存储文件:用户信息
user_config、系统设置app_setting; - 禁止所有键值存入同一个文件,读写效率随数据量增大下降;
- 临时缓存、长期配置分开存储,清空操作互不干扰。
5.2 生命周期释放规范
- 工具内部缓存 pref 实例,避免频繁创建销毁损耗性能;
- 应用完全退出、EntryAbility onDestroy 调用
releaseAll()释放全部文件句柄; - 单个页面临时专用配置可页面销毁单独 release,全局配置缓存复用。
5.3 持久化落盘规范
- 每次 put 操作后必须执行
flush(),API23 不会自动同步本地; - 高频循环写入(循环 for 存数据)可循环结束后统一 flush,减少 IO 次数;
- 重启后数据丢失优先检查是否缺少 flush 调用。
5.4 读写性能规范
- UI 主线程统一使用异步 async/await 接口,禁止同步阻塞渲染;
- 页面 onShow 读取一次缓存到 @State,不要频繁实时读取 pref;
- 大量数组数据不建议存入 Preferences,改用 RDB 或 fs 文件。
5.5 与 AppStorage、PersistentStorage 配合规范
- Preferences:底层持久化数据源;
- PersistentStorage:自动绑定 AppStorage 与 Preferences,简化读写;
- AppStorage:运行时全局内存状态,UI 实时响应修改。
六、API23 升级高频问题与解决方案
问题 1:重启 APP,深色模式 / 登录账号全部丢失 解决:每次 put 后调用pref.flush()强制写入本地文件。
问题 2:多页面同时读写同一配置,数据互相覆盖 解决:工具层单文件复用同一个 pref 实例,内置读写锁串行操作。
问题 3:多次进出页面,存储文件被占用无法清空 解决:应用退出统一调用 releaseAll 释放所有 pref 句柄。
问题 4:存入数组读取为空 解决:使用 putStringArray /getStringArray 专用数组接口,不要拼接 JSON 字符串。
问题 5:升级 API23 旧代码无 flush,存储失效 解决:所有写入方法尾部补充 flush 同步本地文件。
问题 6:读取时上下文为空,存取失败 解决:页面 aboutToAppear 调用 PrefUtil.setContext (getContext (this)) 注入上下文。
七、总结
Preferences 是鸿蒙轻量化配置存储标准组件,API23 完善实例缓存、文件释放、强制落盘、并发读写锁机制,解决旧版配置重启丢失、文件占用、多页面读写错乱等问题。 工具类按业务拆分存储文件,封装布尔、字符串、数字、数组完整读写、删除、清空能力,搭配登录缓存、主题设置两大高频业务页面,可与 AppStorage、PersistentStorage 配合实现「内存全局状态 + 本地永久保存」标准架构,是所有鸿蒙项目基础底层存储工具,适配课程设计、毕设全部小型配置持久化场景。
