成都市网站建设_网站建设公司_阿里云_seo优化
2026/1/16 20:05:44 网站建设 项目流程

文章目录

      • 前言
      • 一、 轻量级存储的选择哲学
      • 二、 封装的核心:告别魔法字符串与 I/O 焦虑
      • 三、 配合 AppStorage 实现响应式配置
      • 四、 总结与实战

前言

在开发任何一个成熟的 App 时,我们都会遇到一类非常琐碎但又至关重要的数据存储需求。比如,用户第一次打开应用时显示的引导页,第二次打开就不应该再出现了;用户在设置里关闭了“消息推送声音”,杀掉进程重启后,这个开关依然得是关闭状态。

这些数据既不需要像用户信息那样存进关系型数据库里进行复杂的 SQL 查询,也不像图片视频那样需要庞大的文件系统来承载。它们只是几个简单的Key-Value键值对,轻量、高频且需要持久化。

在鸿蒙 HarmonyOS 6 (API 20) 中,系统为我们提供了一个专门处理这类需求的模块 用户首选项 Preferences。很多初学者在刚接触时,喜欢在每一个需要存数据的地方都写一遍getPreferences,然后硬编码一堆"is_first_launch"这样的字符串 Key。这种做法在项目初期可能跑得通,但随着业务迭代,散落在各处的魔法字符串和未经管理的 I/O 操作,会让代码变得极其脆弱且难以维护。

今天,我们就来聊聊如何优雅地封装 Preferences,以及在实际开发中那些容易被忽视的性能陷阱。

一、 轻量级存储的选择哲学

我们首先要搞清楚Preferences到底适合存什么。它的底层实现其实是基于内存缓存加上 XML 文件的持久化。这意味着当你调用 API 读取数据时,速度是非常快的,因为它直接从内存里拿;但当你写入数据时,虽然内存是即时更新的,但最终写入磁盘是一个 I/O 操作。

因此,Preferences 的最佳应用场景是“配置型”数据。比如字体大小设置、夜间模式开关、是否已展示隐私协议弹窗等。这些数据通常是 String、Number 或者 Boolean 类型,且数据量极小。千万不要试图用 Preferences 去存一张图片的 Base64 编码,或者一个包含了成百上千条记录的 JSON 字符串。

虽然 API 20 取消了早期版本中严格的 8KB 限制,但如果你存入过大的数据,每次应用启动加载 Preferences 实例时,都会引发显著的内存抖动和启动耗时。

对于结构化的大数据,请出门左转找RDB (关系型数据库);对于文件流,请使用FileFs

二、 封装的核心:告别魔法字符串与 I/O 焦虑

在原生 API 中,使用 Preferences 的流程通常是:获取 Context -> 获取 Preferences 实例 -> 调用 put 方法 ->调用 flush 方法。这一连串动作里,最容易出问题的就是最后一步flush

很多开发者在写代码时,习惯性地写完put就以为万事大吉了。实际上,put操作仅仅是修改了内存中的对象,如果没有调用flush,数据是不会写入磁盘文件的。如果此时应用崩溃或者被系统强杀,你刚才存的数据就丢了。但是,如果我们每次put之后都立即await flush(),在高频操作下又会造成大量的磁盘 I/O,导致界面卡顿。

所以,我们需要构建一个PreferenceManager单例类。这个类需要解决两个核心问题:第一,统一管理所有的 Key,严禁在业务代码里裸写字符串;第二,封装putflush的逻辑。我们可以利用 TypeScript 的泛型和枚举能力,让键值对的读写变成类型安全的操作。比如,我们可以定义一个枚举StorageKeys,在调用save方法时,入参必须是这个枚举的成员,这样 IDE 就能帮我们自动补全,彻底杜绝了因为手抖把"user_age"写成"user_agr"而导致的 Bug。

此外,在 API 20 中,Preferences 的实例获取需要依赖Context。为了避免在每个页面都去重复获取 Context,我们可以在EntryAbilityonCreate阶段就初始化这个单例,持有全局的 Context,这样后续在任何 UI 组件或逻辑类中,都可以直接调用PreferenceManager.getInstance().getValue(...),极其方便。

三、 配合 AppStorage 实现响应式配置

单独使用 Preferences 只能解决“存”和“取”的问题,但在 UI 开发中,我们更关心的是“变”。当用户在设置页修改了字号,首页的列表应该立即感知并刷新。虽然 Preferences 提供了on('change')监听器,但让每个页面都去注册监听器显然太繁琐了。

最佳的实践是将Preferences作为AppStorage的持久化后端。当应用启动时,我们从 Preferences 读取所有配置项,一次性写入 AppStorage。之后的业务中,UI 组件只通过@StorageLink与 AppStorage 交互。当用户修改配置时,我们同时更新 AppStorage(驱动 UI 刷新)和 Preferences(负责存盘)。

这样,我们既享受了 AppStorage 的响应式便利,又拥有了 Preferences 的持久化能力。虽然系统提供的PersistentStorage也能做类似的事,但手动封装 Preferences 能让我们对数据的迁移(比如 App 升级后旧配置的兼容处理)拥有 100% 的控制权,这在商业级项目中是非常必要的。

import { preferences } from '@kit.ArkData'; import { common } from '@kit.AbilityKit'; import { promptAction } from '@kit.ArkUI'; // ------------------------------------------------------------- // 1. 定义 Key 的枚举与常量 // ------------------------------------------------------------- export enum AppStorageKeys { IS_FIRST_LAUNCH = 'is_first_launch', USER_FONT_SIZE = 'user_font_size', ENABLE_NIGHT_MODE = 'enable_night_mode', USER_NAME = 'user_name' } // ------------------------------------------------------------- // 2. Preferences 管理单例封装 // ------------------------------------------------------------- class PreferenceManager { private static instance: PreferenceManager; private pref: preferences.Preferences | null = null; private readonly PREF_NAME = 'my_app_prefs'; private constructor() {} public static getInstance(): PreferenceManager { if (!PreferenceManager.instance) { PreferenceManager.instance = new PreferenceManager(); } return PreferenceManager.instance; } /** * 初始化方法,通常在 Ability 的 onCreate 中调用 * @param context UIAbilityContext */ public async init(context: common.UIAbilityContext): Promise<void> { try { // 获取 Preferences 实例 // 注意:getPreferences 是异步方法 this.pref = await preferences.getPreferences(context, this.PREF_NAME); console.info('[PreferenceManager] Initialized success'); } catch (err) { console.error('[PreferenceManager] Failed to get preferences', JSON.stringify(err)); } } /** * 保存数据 (自动处理 flush) * @param key 存储键 * @param value 存储值 (支持 string | number | boolean | Array<string>) */ public async setValue(key: AppStorageKeys, value: preferences.ValueType): Promise<void> { if (!this.pref) { console.error('[PreferenceManager] Not initialized, call init() first'); return; } try { await this.pref.put(key, value); // 关键步骤:每次 put 后必须 flush 才能写入磁盘 // 在高频写入场景(如 Slider 拖动),建议在页面销毁或特定时机统一 flush,而非每次都 flush await this.pref.flush(); console.info(`[PreferenceManager] Saved: ${key} = ${value}`); } catch (err) { console.error(`[PreferenceManager] Failed to put value for ${key}`, JSON.stringify(err)); } } /** * 读取数据 * @param key 存储键 * @param defaultValue 默认值 */ public async getValue<T extends preferences.ValueType>(key: AppStorageKeys, defaultValue: T): Promise<T> { if (!this.pref) { console.error('[PreferenceManager] Not initialized'); return defaultValue; } try { const value = await this.pref.get(key, defaultValue); return value as T; } catch (err) { console.error(`[PreferenceManager] Failed to get value for ${key}`, JSON.stringify(err)); return defaultValue; } } /** * 删除指定 Key */ public async deleteValue(key: AppStorageKeys): Promise<void> { if (!this.pref) return; try { await this.pref.delete(key); await this.pref.flush(); } catch (err) { console.error(`[PreferenceManager] Failed to delete ${key}`, JSON.stringify(err)); } } } // 导出单例供页面使用 export const prefManager = PreferenceManager.getInstance(); // ------------------------------------------------------------- // 3. 实战页面:用户偏好设置 // ------------------------------------------------------------- @Entry @Component struct PreferencesDemoPage { // 使用 @State 驱动 UI,数据加载后会同步更新这里 @State isNightMode: boolean = false; @State fontSize: number = 16; @State userName: string = ''; @State isLoading: boolean = true; // 模拟初始化加载 async aboutToAppear() { // 【关键】获取 UIAbilityContext // 在真实项目中,建议在 EntryAbility.ts 的 onCreate 中初始化 prefManager // 这里为了演示方便,在页面加载时进行初始化 const context = getContext(this) as common.UIAbilityContext; await prefManager.init(context); // 读取持久化的数据 // 注意:await 会等待异步读取完成,此时页面可能尚未渲染完毕 this.isNightMode = await prefManager.getValue(AppStorageKeys.ENABLE_NIGHT_MODE, false); this.fontSize = await prefManager.getValue(AppStorageKeys.USER_FONT_SIZE, 16); this.userName = await prefManager.getValue(AppStorageKeys.USER_NAME, ''); this.isLoading = false; } build() { Column() { // 顶部标题 Text('偏好设置持久化') .fontSize(24) .fontWeight(FontWeight.Bold) .margin({ top: 40, bottom: 20 }) .fontColor(this.isNightMode ? Color.White : Color.Black) .animation({ duration: 300 }) if (this.isLoading) { LoadingProgress() .width(50) .height(50) .color(Color.Blue) } else { Column({ space: 20 }) { // ------------------------------------------------ // 1. 夜间模式开关 // ------------------------------------------------ Row() { Column() { Text('夜间模式') .fontSize(18) .fontWeight(FontWeight.Medium) .fontColor(this.isNightMode ? Color.White : Color.Black) Text('重启 App 后依然生效') .fontSize(12) .fontColor('#999') .margin({ top: 4 }) } .alignItems(HorizontalAlign.Start) Toggle({ type: ToggleType.Switch, isOn: this.isNightMode }) .selectedColor('#0A59F7') // 鸿蒙蓝 .onChange(async (isOn: boolean) => { this.isNightMode = isOn; // 持久化保存 await prefManager.setValue(AppStorageKeys.ENABLE_NIGHT_MODE, isOn); promptAction.showToast({ message: isOn ? '已开启夜间模式' : '已关闭夜间模式' }); }) } .width('100%') .justifyContent(FlexAlign.SpaceBetween) .padding(16) .backgroundColor(this.isNightMode ? '#333' : '#FFF') .borderRadius(16) .shadow({ radius: 8, color: '#1A000000' }) .animation({ duration: 300 }) // ------------------------------------------------ // 2. 字体大小调节 // ------------------------------------------------ Column() { Text(`字体大小: ${this.fontSize.toFixed(0)}`) .fontSize(16) .fontColor(this.isNightMode ? Color.White : Color.Black) .width('100%') .margin({ bottom: 10 }) Slider({ value: this.fontSize, min: 12, max: 30, step: 2, style: SliderStyle.OutSet }) .blockColor('#0A59F7') .trackColor('#E0E0E0') .selectedColor('#0A59F7') .onChange(async (value: number, mode: SliderChangeMode) => { this.fontSize = value; // 优化:仅在拖动结束 (End) 或 点击 (Click) 时写入磁盘,避免高频 IO if (mode === SliderChangeMode.End || mode === SliderChangeMode.Click) { await prefManager.setValue(AppStorageKeys.USER_FONT_SIZE, value); } }) } .padding(16) .backgroundColor(this.isNightMode ? '#333' : '#FFF') .borderRadius(16) .shadow({ radius: 8, color: '#1A000000' }) .animation({ duration: 300 }) // ------------------------------------------------ // 3. 用户名输入 (自动保存) // ------------------------------------------------ Column() { Text('用户名 (输入即保存)') .fontSize(16) .fontColor(this.isNightMode ? Color.White : Color.Black) .width('100%') .margin({ bottom: 10 }) TextInput({ text: this.userName, placeholder: '请输入名字' }) .backgroundColor(this.isNightMode ? '#444' : '#F5F5F5') .fontColor(this.isNightMode ? Color.White : Color.Black) .placeholderColor(this.isNightMode ? '#888' : '#CCC') .padding({ left: 12 }) .height(48) .borderRadius(8) .onChange(async (value: string) => { this.userName = value; // 实际开发中建议做防抖 (Debounce) await prefManager.setValue(AppStorageKeys.USER_NAME, value); }) } .padding(16) .backgroundColor(this.isNightMode ? '#333' : '#FFF') .borderRadius(16) .shadow({ radius: 8, color: '#1A000000' }) .animation({ duration: 300 }) // ------------------------------------------------ // 4. 预览效果 // ------------------------------------------------ Text('这是一段预览文字,用于展示字体大小和夜间模式的效果。用户首选项不仅能保存简单的开关,也能保存用户的个性化配置。') .fontSize(this.fontSize) // 动态应用字号 .fontColor(this.isNightMode ? '#EEE' : '#333') .margin({ top: 20 }) .lineHeight(this.fontSize * 1.5) .animation({ duration: 300 }) } .width('90%') } } .width('100%') .height('100%') .backgroundColor(this.isNightMode ? '#1a1a1a' : '#F1F3F5') .animation({ duration: 300 }) // 全局背景色切换动画 } }

四、 总结与实战

Preferences 是鸿蒙应用中最不起眼但使用率最高的组件之一。

用好它,关键在于克制规范。克制是指不要滥用它存大数据,规范是指通过封装来隔离底层 API 的复杂性。

一个优秀的数据持久化层,应该像空气一样,平时感觉不到它的存在,但在你每次打开 App 时,它都能精准地还原你上次离开时的样子。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询