【学习目标】

  1. 理解 Preferences 核心定位,明确 XML/GSKV 两种存储模式的差异与适用场景;
  2. 掌握 Preferences 使用方法,实现复杂对象的持久化存储,解决 PersistentStorage 仅支持基础类型的局限;
  3. 掌握 Preferences 异常处理、多进程适配、性能优化的最佳实践;
  4. 封装通用 Preferences 工具类,适配不同存储模式,实现工程化复用;
  5. 验证「应用重启后数据仍留存」的核心特性,规避核心使用坑点。

一、回顾与本节引入

上一节我们掌握了 PersistentStorage 基础持久化能力,但该方案存在核心局限:

  • 仅支持基础类型(number/string/boolean)和简单 JSON 对象存储;
  • 单文件数据量限制 2KB,无法存储稍复杂的业务配置;
  • 仅适用于轻量核心状态持久化,扩展性不足。

为解决上述问题,鸿蒙系统提供了更强大的轻量级持久化方案——Preferences(用户首选项)

  • 支持复杂对象/数组的序列化存储,单 Value 最大支持 16MB;
  • 提供 XML(默认)、GSKV(API 18+)两种存储模式,适配单/多进程场景;
  • 数据持久化到应用沙箱目录,重启应用后数据不丢失;
  • 专为应用配置、用户个性化设置等轻量数据设计。

二、工程结构(API18+ 核心目录)

PreferencesDemo/
├── AppScope/
│   └── app.json5                      // 应用全局配置(应用名称、权限等)
├── entry/
│   ├── src/ 
│   │   ├── main/
│   │   │   ├── ets/
│   │   │   │   ├── entryability/
│   │   │   │   │   └── EntryAbility.ets       // 应用入口(初始化Preferences Context)
│   │   │   │   ├── common/                    // 通用工具目录
│   │   │   │   │   ├── constants/             // 常量定义
│   │   │   │   │   │   └── PreferencesConstants.ets // 偏好设置常量(文件名/存储键)
│   │   │   │   │   └── utils/                 // 工具类
│   │   │   │   │       └── PreferencesUtil.ets // Preferences单例工具类(核心封装)
│   │   │   │   ├── components/                // 通用组件目录(ets根目录下)
│   │   │   │   │   ├── DataDisplay.ets        // 数据展示组件(用户名/登录状态)
│   │   │   │   │   └── ThemeSelector.ets      // 主题选择组件(浅色/深色/系统)
│   │   │   │   ├── model/                     // 数据模型目录
│   │   │   │   │   └── User.ets               // 用户信息模型(@Observed标记)
│   │   │   │   └── pages/                     // 页面目录
│   │   │   │       └── Index.ets              // 核心演示页面(整合组件/测试功能)
│   │   │   ├── resources/                     // 资源目录(鸿蒙标准)
│   │   │   └── module.json5                   // 模块配置(页面路由/权限)
│   │   └── oh-package.json5                   // 项目依赖配置(npm包/鸿蒙库)
│   ├── build-profile.json5                    // 构建配置(编译选项/签名)
│   └── hvigorfile.ts                          // 构建脚本(鸿蒙工程标准)
└── hvigorfile.ts                              // 全局构建脚本

三、Preferences 核心认知

3.1 核心定位

Preferences(用户首选项)是鸿蒙系统提供的 Key-Value 型轻量级数据持久化存储方案,核心用于存储应用配置、用户个性化设置等轻量数据。其提供两种存储模式:

  • XML 模式(默认):数据存储为 XML 文件,单进程安全,需手动刷盘;
  • GSKV 模式(API 18+):数据存储为 GSKV 格式,支持多进程并发,自动刷盘。

3.2 核心特性

特性 说明
存储形式 Key-Value 键值对(Key 为非空字符串,≤1024 字节)
支持类型 基础类型(string/number/boolean/Uint8Array)、可序列化复杂对象(JSON 格式)
数据量级 单 Value 最大支持 16MB
读写方式 同步/异步(推荐异步,避免阻塞UI)
重启留存 刷盘后持久化到沙箱,重启仍存在
安全特性 沙箱隔离(仅应用自身可访问)

3.3 模式约束与限制

3.3.1 XML 模式(默认)
  1. 进程安全限制:无法保证进程并发安全,多进程读写会导致文件损坏、数据丢失,禁止在多进程场景下使用
  2. 字符编码限制:存储非 UTF-8 格式字符串时,必须使用 Uint8Array 类型存储,否则会导致持久化文件格式错误、文件损坏;
  3. 内存与性能限制
    • 内存占用随存储数据量线性增长,建议单文件存储数据不超过 50MB(轻量级存储定位);
    • 数据量较大时,同步接口创建实例/持久化数据会成为耗时操作,禁止在主线程使用,否则会触发 appfreeze 问题;
  4. 刷盘约束:所有数据操作仅生效于内存,需手动调用 flush() 刷盘,否则应用重启后数据丢失;
  5. 通用约束:单 Value 超过 16MB 会写入失败,需拆分存储;deletePreferences 接口不可与其他 Preferences 接口并发调用。
3.3.2 GSKV 模式
  1. 版本与平台限制:仅鸿蒙 API 18 及以上版本支持,且不支持跨平台,使用前必须通过 isStorageTypeSupported 判断当前平台是否支持;
  2. 自动刷盘特性:无需手动调用 flush(),数据操作后自动持久化,但刷盘时机由系统控制,高频写操作可能存在短暂的内存/文件数据不一致;
  3. 数据量级约束:同样遵循单 Value 不超过 16MB、单文件建议不超过 50MB 的轻量级存储原则;
  4. 通用约束:不提供数据加密能力,敏感数据需自行加密后存储;deletePreferences 接口不可与其他 Preferences 接口并发调用。

3.4 核心接口清单

接口分类 接口名称 作用描述
实例管理 getPreferencesSync 同步获取 Preferences 实例(存在异步接口 getPreferences
实例管理 deletePreferences 异步删除指定 Preferences 实例及持久化文件(回调形式)
实例管理 isStorageTypeSupported 同步判断当前平台是否支持指定存储模式(XML/GSKV)
数据操作 putSync 同步写入 Key-Value 数据(仅内存生效,存在异步接口 put
数据操作 getSync 同步读取指定 Key 的数据(无值返回默认值,存在异步接口 get
数据操作 hasSync 同步检查指定 Key 是否存在(存在异步接口 has
数据操作 deleteSync 同步删除指定 Key 的数据(仅内存生效,存在异步接口 delete
数据操作 flush 异步刷盘(将内存数据写入文件,仅 XML 模式有效,回调形式)
监听操作 on('change') 订阅数据变更(刷盘后触发,回调参数为单个 Key 字符串)
监听操作 off('change') 取消订阅数据变更

四、工程化封装:Preferences 工具类

4.1 定义常量

// common/constants/PreferencesConstants.ets
import { preferences } from '@kit.ArkData';

/** 偏好设置文件名(对应沙箱中实际存储的文件) */
export const PREFERENCE_FILE_NAME = 'app_user_prefs';
/** 用户信息存储键(唯一标识) */
export const USER_INFO_KEY = 'user_info';
/** 默认存储模式:XML */
export const DEFAULT_STORAGE_TYPE = preferences.StorageType.XML;

4.2 封装完整工具类(common/utils/PreferencesUtil.ets)

import { preferences, ValueType } from '@kit.ArkData';
import { PREFERENCE_FILE_NAME, DEFAULT_STORAGE_TYPE } from '../constants/PreferencesConstants';

/**
 * Preferences 工具类(单例模式)
 * 适配 XML/GSKV 双模式,包含完整异常处理
 */
class PreferencesUtil {
  // 单例实例(全局唯一)
  private static instance: PreferencesUtil | null = null;
  // Preferences 核心实例(缓存)
  private prefs: preferences.Preferences | null = null;
  // 应用上下文(必须初始化)
  private appContext: Context | null = null;
  // 存储模式
  private storageType: preferences.StorageType = DEFAULT_STORAGE_TYPE;
  // 数据变化监听回调缓存(用于取消监听)
  private changeCallbacks: Map<string, (key: string) => void> = new Map();

  // 私有构造函数,禁止外部实例化
  private constructor() {}

  /**
   * 获取单例实例
   * @returns PreferencesUtil 实例
   */
  public static getInstance(): PreferencesUtil {
    if (!PreferencesUtil.instance) {
      PreferencesUtil.instance = new PreferencesUtil();
    }
    return PreferencesUtil.instance;
  }

  /**
   * 初始化应用上下文(必须在 EntryAbility 中调用)
   * @param context 应用/Ability 上下文
   * @param storageType 存储模式(可选,默认 XML)
   */
  public initWithContext(context: Context, storageType?: preferences.StorageType): void {
    if (!this.appContext && context) {
      this.appContext = context;
      this.storageType = storageType || DEFAULT_STORAGE_TYPE;
      console.info(`[PreferencesUtil] Context 初始化成功,存储模式:${this.storageType === preferences.StorageType.XML ? 'XML' : 'GSKV'}`);
    }
  }

  /**
   * 初始化 Preferences 实例(同步,内部调用)
   * @returns Preferences 实例 | null
   */
  private initPrefs(): preferences.Preferences | null {
    // 校验上下文是否初始化
    if (!this.appContext) {
      console.error('[PreferencesUtil] 错误:未初始化 Context,请先调用 initWithContext');
      return null;
    }

    // 缓存实例,避免重复创建
    if (this.prefs) {
      return this.prefs;
    }

    try {
      // GSKV 模式兼容性判断
      if (this.storageType === preferences.StorageType.GSKV) {
        const isSupport = preferences.isStorageTypeSupported(preferences.StorageType.GSKV);
        if (!isSupport) {
          console.warn('[PreferencesUtil] GSKV 模式不支持,自动降级为 XML 模式');
          this.storageType = preferences.StorageType.XML;
        }
      }

      // 同步获取 Preferences 实例
      this.prefs = preferences.getPreferencesSync(this.appContext, {
        name: PREFERENCE_FILE_NAME,
        storageType: this.storageType
      });
      console.info(`[PreferencesUtil] Preferences 实例初始化成功(模式:${this.storageType === preferences.StorageType.XML ? 'XML' : 'GSKV'}`);
      return this.prefs;
    } catch (err) {
      console.error(`[PreferencesUtil] 实例初始化失败:${(err as Error).message}`);
      return null;
    }
  }

  // ==================== 基础数据操作 API ====================
  /**
   * 写入数据
   * @param key 存储键(非空)
   * @param value 存储值(基础类型/JSON字符串/Uint8Array)
   * @returns 是否写入成功
   */
  public put(key: string, value: ValueType): boolean {
    try {
      const prefs = this.initPrefs();
      if (!prefs || !key) return false;

      prefs.putSync(key, value);
      console.info(`[PreferencesUtil] 写入数据成功:${key}=${JSON.stringify(value)}`);
      return true;
    } catch (err) {
      console.error(`[PreferencesUtil] 写入失败 [${key}]:${(err as Error).message}`);
      return false;
    }
  }

  /**
   * 读取数据
   * @param key 存储键
   * @param defValue 默认值(读取失败/无数据时返回)
   * @returns 读取到的数据 | 默认值
   */
  public get<T extends ValueType>(key: string, defValue: T): T {
    try {
      const prefs = this.initPrefs();
      if (!prefs || !key) return defValue;

      const value = prefs.getSync(key, defValue);
      console.info(`[PreferencesUtil] 读取数据成功:${key}=${JSON.stringify(value)}`);
      return value as T;
    } catch (err) {
      console.error(`[PreferencesUtil] 读取失败 [${key}]:${(err as Error).message}`);
      return defValue;
    }
  }

  /**
   * 检查键是否存在
   * @param key 存储键
   * @returns 是否存在
   */
  public has(key: string): boolean {
    try {
      const prefs = this.initPrefs();
      if (!prefs || !key) return false;

      const exists = prefs.hasSync(key);
      console.info(`[PreferencesUtil] 检查键 [${key}] 存在:${exists}`);
      return exists;
    } catch (err) {
      console.error(`[PreferencesUtil] 检查键失败 [${key}]:${(err as Error).message}`);
      return false;
    }
  }

  /**
   * 删除指定键数据
   * @param key 存储键
   * @returns 是否删除成功
   */
  public delete(key: string): boolean {
    try {
      const prefs = this.initPrefs();
      if (!prefs || !key) return false;

      prefs.deleteSync(key);
      console.info(`[PreferencesUtil] 删除数据成功:${key}`);
      return true;
    } catch (err) {
      console.error(`[PreferencesUtil] 删除失败 [${key}]:${(err as Error).message}`);
      return false;
    }
  }

  // ==================== 异步操作 API ====================
  /**
   * 异步刷盘(仅 XML 模式有效)
   * @param callback 刷盘完成回调(可选,err: 错误信息 | null)
   */
  public flush(callback?: (err: Error | null) => void): void {
    try {
      const prefs = this.initPrefs();
      if (!prefs) {
        console.error('[PreferencesUtil] 刷盘失败:Preferences 实例未初始化');
        callback?.(new Error('Preferences 实例未初始化'));
        return;
      }

      prefs.flush((err) => {
        if (err) {
          console.error(`[PreferencesUtil] 刷盘失败:${JSON.stringify(err)}`);
          callback?.(new Error(`刷盘失败:${err.message}`));
        } else {
          console.info('[PreferencesUtil] 刷盘成功');
          callback?.(null);
        }
      });
    } catch (err) {
      console.error(`[PreferencesUtil] 刷盘异常:${(err as Error).message}`);
      callback?.(err as Error);
    }
  }

  /**
   * 订阅数据变更
   * @param callback 变更回调(key: 变更的单个键字符串)
   * @returns 监听ID(用于取消监听)
   */
  public onDataChange(callback: (key: string) => void): string {
    try {
      const prefs = this.initPrefs();
      if (!prefs) {
        console.error('[PreferencesUtil] 订阅失败:Preferences 实例未初始化');
        return '';
      }

      // 生成唯一监听ID
      const listenerId = Date.now().toString();
      const changeCallback = (key: string) => {
        console.info(`[PreferencesUtil] 数据变更监听触发:${key}`);
        callback(key);
      };

      prefs.on('change', changeCallback);
      this.changeCallbacks.set(listenerId, changeCallback);
      console.info(`[PreferencesUtil] 数据变更订阅成功,监听ID:${listenerId}`);
      return listenerId;
    } catch (err) {
      console.error(`[PreferencesUtil] 订阅失败:${(err as Error).message}`);
      return '';
    }
  }

  /**
   * 取消数据变更订阅
   * @param listenerId 监听ID
   */
  public offDataChange(listenerId: string): void {
    try {
      const prefs = this.initPrefs();
      if (!prefs || !this.changeCallbacks.has(listenerId)) {
        console.warn('[PreferencesUtil] 取消订阅失败:监听ID不存在或实例未初始化');
        return;
      }

      // 获取缓存的回调函数
      const callback = this.changeCallbacks.get(listenerId);
      prefs.off('change', callback);
      this.changeCallbacks.delete(listenerId);
      console.info(`[PreferencesUtil] 取消订阅成功,监听ID:${listenerId}`);
    } catch (err) {
      console.error(`[PreferencesUtil] 取消订阅失败 [${listenerId}]:${(err as Error).message}`);
    }
  }

  /**
   * 删除 Preferences 文件
   * @param callback 操作完成回调(可选,err: 错误信息 | null)
   * @returns 是否触发删除操作成功
   */
  public deletePreferencesFile(callback?: (err: Error | null) => void): boolean {
    try {
      if (!this.appContext) {
        const err = new Error('Context 未初始化');
        console.error(err.message);
        callback?.(err);
        return false;
      }

      preferences.deletePreferences(this.appContext, {
        name: PREFERENCE_FILE_NAME,
        storageType: this.storageType
      }, (err) => {
        if (err) {
          console.error(`[PreferencesUtil] 删除文件失败:${JSON.stringify(err)}`);
          callback?.(new Error(`删除失败:${err.message}`));
        } else {
          this.prefs = null; // 清空缓存实例
          console.info(`[PreferencesUtil] Preferences 文件 ${PREFERENCE_FILE_NAME} 删除成功`);
          callback?.(null);
        }
      });
      return true;
    } catch (err) {
      const error = err as Error;
      console.error(`[PreferencesUtil] 删除文件异常:${error.message}`);
      callback?.(error);
      return false;
    }
  }

  // ==================== 便捷操作 API ====================
  /**
   * 写入数据并自动刷盘(XML 模式专用)
   * @param key 存储键
   * @param value 存储值
   * @param callback 完成回调(可选,err: 错误信息 | null)
   * @returns 是否操作成功(true=成功,false=失败)
   */
  public putAndFlush(key: string, value: ValueType, callback?: (err: Error | null) => void): boolean {
    // 第一步:写入数据
    const putSuccess = this.put(key, value);
    if (!putSuccess) {
      const err = new Error(`写入数据失败:${key}`);
      console.error(err.message);
      callback?.(err);
      return false;
    }

    // 第二步:区分存储模式处理刷盘
    if (this.storageType === preferences.StorageType.XML) {
      // XML模式:异步刷盘,通过回调返回结果
      this.flush((err) => {
        if (err) {
          console.error(`[PreferencesUtil] 刷盘失败:${err.message}`);
        }
        callback?.(err);
      });
    } else {
      // GSKV模式:自动刷盘,直接返回成功
      callback?.(null);
    }

    // 写入成功即返回true(刷盘失败不影响写入结果,仅通过回调通知)
    return true;
  }

  /**
   * 删除数据并自动刷盘(XML 模式专用)
   * @param key 存储键
   * @param callback 完成回调(可选,err: 错误信息 | null)
   * @returns 是否操作成功(true=成功,false=失败)
   */
  public deleteAndFlush(key: string, callback?: (err: Error | null) => void): boolean {
    // 第一步:删除数据
    const deleteSuccess = this.delete(key);
    if (!deleteSuccess) {
      const err = new Error(`删除数据失败:${key}`);
      console.error(err.message);
      callback?.(err);
      return false;
    }

    // 第二步:区分存储模式处理刷盘
    if (this.storageType === preferences.StorageType.XML) {
      // XML模式:异步刷盘,通过回调返回结果
      this.flush((err) => {
        if (err) {
          console.error(`[PreferencesUtil] 刷盘失败:${err.message}`);
        }
        callback?.(err);
      });
    } else {
      // GSKV模式:自动刷盘,直接返回成功
      callback?.(null);
    }

    // 删除成功即返回true
    return true;
  }
}

// 导出单例实例(全局使用)
export const PreferencesManager = PreferencesUtil.getInstance();

4.3 应用入口初始化 Context(entryability/EntryAbility.ets)

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { PreferencesManager } from '../common/utils/PreferencesUtil';
import { preferences } from '@kit.ArkData';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 初始化 Preferences 上下文
    // 示例1:使用默认 XML 模式
    PreferencesManager.initWithContext(this.context);
    
    // 示例2:尝试使用 GSKV 模式(API 18+)
    // PreferencesManager.initWithContext(this.context, preferences.StorageType.GSKV);
    
    console.info('EntryAbility onCreate:Preferences Context 初始化完成');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    windowStage.loadContent('pages/Index', (err, data) => {
      if (err.code) {
        console.error('Failed to load the content. Cause: %{public}s', JSON.stringify(err));
        return;
      }
      console.info('Succeeded in loading the content. Data: %{public}s', JSON.stringify(data));
    });
  }
}

4.4 数据模型与组件(核心代码)

4.4.1 数据模型(model/User.ets)
/**
 * 用户设置子模型(需@Observed标记,支持@ObjectLink深度监听)
 */
@Observed
export class UserSettings {
  // 主题模式(light/ dark/ system)
  themeMode: 'light' | 'dark' | 'system';

  /**
   * 构造函数
   * @param themeMode 主题模式(默认跟随系统)
   */
  constructor(themeMode: 'light' | 'dark' | 'system' = 'system') {
    this.themeMode = themeMode;
  }
}

/**
 * 用户信息主模型(嵌套对象,需@Observed标记)
 */
@Observed
export class User {
  // 用户名
  name: string;
  // 登录状态
  isLogin: boolean;
  // 嵌套用户设置
  settings: UserSettings;

  /**
   * 构造函数
   * @param name 用户名(默认"未登录")
   * @param isLogin 登录状态(默认false)
   * @param settings 用户设置(默认新建实例)
   */
  constructor(
    name: string = "未登录",
    isLogin: boolean = false,
    settings?: UserSettings
  ) {
    this.name = name;
    this.isLogin = isLogin;
    this.settings = settings || new UserSettings();
  }
}
4.4.2 数据展示组件(components/DataDisplay.ets)
import { User } from "../model/User";

@Component
export default struct DataDisplay {
  // 深度监听User对象(需父组件传入@State标记的实例)
  @ObjectLink userInfo: User;

  build() {
    Column({ space: 8 }) {
      // 用户名输入框
      TextInput({
        placeholder: "请输入用户名",
        text: this.userInfo.name
      })
        .onChange((value) => {
          this.userInfo.name = value; // 直接修改,UI自动刷新
        })
        .width('80%')
        .height(40)
        .border({ width: 1, color: '#ccc' });

      // 用户名展示
      Text(`当前用户名:${this.userInfo.name}`)
        .fontSize(16)
        .fontColor('#333');

      // 登录状态展示
      Text(`登录状态:${this.userInfo.isLogin ? '已登录' : '未登录'}`)
        .fontSize(16)
        .fontColor('#666');

      // 登录按钮
      Button("点击登录")
        .onClick(() => {
          this.userInfo.isLogin = true;
        })
        .margin({ top: 8 })
        .backgroundColor('#007DFF')
        .fontColor('#fff');
    }
  }
}
4.4.3 主题选择组件(components/ThemeSelector.ets)
import { UserSettings } from "../model/User";

@Component
export default struct ThemeSelector {
  // 深度监听UserSettings对象
  @ObjectLink settings: UserSettings;

  build() {
    Column({ space: 10 }) {
      // 当前主题展示
      Text(`当前主题:${this.settings.themeMode}`)
        .fontSize(18)
        .fontWeight(FontWeight.Medium);

      // 主题选择按钮组
      Row({ space: 10 }) {
        Button('浅色模式')
          .backgroundColor(this.settings.themeMode === 'light' ? '#007DFF' : '#eee')
          .fontColor(this.settings.themeMode === 'light' ? '#fff' : '#333')
          .onClick(() => {
            this.settings.themeMode = 'light';
          });

        Button('深色模式')
          .backgroundColor(this.settings.themeMode === 'dark' ? '#007DFF' : '#eee')
          .fontColor(this.settings.themeMode === 'dark' ? '#fff' : '#333')
          .onClick(() => {
            this.settings.themeMode = 'dark';
          });

        Button('跟随系统')
          .backgroundColor(this.settings.themeMode === 'system' ? '#007DFF' : '#eee')
          .fontColor(this.settings.themeMode === 'system' ? '#fff' : '#333')
          .onClick(() => {
            this.settings.themeMode = 'system';
          });
      }
    }
  }
}

4.5 核心演示页面(pages/Index.ets)

import { PreferencesManager } from '../common/utils/PreferencesUtil';
import { User, UserSettings } from '../model/User';
import { USER_INFO_KEY } from '../common/constants/PreferencesConstants';
import prompt from '@ohos.promptAction';
import DataDisplay from '../components/DataDisplay';
import ThemeSelector from '../components/ThemeSelector';

@Entry
@Component
struct Index {
  // 页面状态数据
  @State userInfo: User = new User("未登录", false, new UserSettings('system'));

  /**
   * 页面加载时自动读取已存储的数据
   */
  aboutToAppear() {
    console.info('页面加载,读取已存储的数据');
    try {
      // 1. 序列化默认实例
      const defaultUserJson = JSON.stringify(this.userInfo);
      // 2. 读取存储的用户数据
      const userJson = PreferencesManager.get(USER_INFO_KEY, defaultUserJson);
      // 3. 反序列化并重建@Observed实例(关键:保证UI响应式)
      const plainObj = JSON.parse(userJson) as User;
      const settings = new UserSettings(plainObj.settings?.themeMode || 'system');
      this.userInfo = new User(
        plainObj.name || "未登录",
        plainObj.isLogin || false,
        settings
      );
    } catch (err) {
      console.error('读取/解析数据失败', err);
      prompt.showToast({ message: "数据加载失败,使用默认值" });
      this.userInfo = new User("未登录", false, new UserSettings('system'));
    }
  }

  /**
   * 保存用户信息(自动刷盘)
   */
  saveUserInfo() {
    if (!this.userInfo.name || this.userInfo.name.trim() === "") {
      prompt.showToast({ message: "请输入有效的用户名" });
      return;
    }

    try {
      // 1. 序列化Class实例
      const userJson = JSON.stringify(this.userInfo);
      // 2. 写入并自动刷盘
      const isSuccess = PreferencesManager.putAndFlush(USER_INFO_KEY, userJson);

      if (isSuccess) {
        prompt.showToast({ message: "用户信息保存成功!重启应用后仍可读取" });
        // 重建实例保证响应式
        this.userInfo = new User(
          this.userInfo.name,
          this.userInfo.isLogin,
          new UserSettings(this.userInfo.settings.themeMode)
        );
      } else {
        prompt.showToast({ message: "保存失败" });
      }
    } catch (err) {
      console.error('保存数据失败', err);
      prompt.showToast({ message: "保存异常:" + (err as Error).message });
    }
  }

  /**
   * 清空用户数据(自动刷盘)
   */
  clearAllData() {
    const isSuccess = PreferencesManager.deleteAndFlush(USER_INFO_KEY);
    if (isSuccess) {
      // 重置为默认值
      this.userInfo = new User("未登录", false, new UserSettings('system'));
      prompt.showToast({ message: "数据已清空" });
    } else {
      prompt.showToast({ message: "清空数据失败" });
    }
  }

  /**
   * 删除Preferences文件
   */
  deleteFile() {
    PreferencesManager.deletePreferencesFile((err) => {
      if (err) {
        prompt.showToast({ message: "删除文件失败:" + err.message });
      } else {
        // 重置为默认值
        this.userInfo = new User("未登录", false, new UserSettings('system'));
        prompt.showToast({ message: "文件已删除" });
      }
    });
  }

  build() {
    Column({ space: 20 }) {
      // 标题
      Text('Preferences 嵌套类持久化演示')
        .fontSize(28)
        .fontWeight(FontWeight.Bold);

      // 数据展示组件
      DataDisplay({ userInfo: this.userInfo });

      // 主题选择组件
      ThemeSelector({ settings: this.userInfo.settings });

      // 操作按钮组
      Button("保存用户信息")
        .onClick(() => this.saveUserInfo())
        .width('80%')
        .backgroundColor($r('sys.color.brand'));

      Button("清空数据")
        .onClick(() => this.clearAllData())
        .width('80%')
        .backgroundColor('#FF4444')
        .fontColor('#fff');

      Button("删除文件")
        .onClick(() => this.deleteFile())
        .width('80%')
        .backgroundColor('#FF4444')
        .fontColor('#fff');

      // 测试提示
      Text('★ 测试步骤:1.输入用户名/选主题 2.保存 3.重启应用 4.验证数据仍留存')
        .fontSize(14)
        .fontColor('#999')
        .margin({ top: 30 });

    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .padding(20);
  }
}

五、核心规则与验证步骤

5.1 核心使用规则

  1. Context 初始化必做:在 EntryAbility 的 onCreate 中调用 PreferencesManager.initWithContext(this.context),这是所有操作的前提;
  2. 存储模式选择
    • 单进程场景:使用 XML 模式(默认),所有写入/删除操作后必须调用 flush() 刷盘(推荐使用 putAndFlush/deleteAndFlush 便捷方法);
    • 多进程场景:仅 API 18+ 且 isStorageTypeSupported(GSKV) 返回 true 时使用 GSKV 模式,无需手动刷盘;
  3. 数据存储规范
    • 复杂对象需序列化为 JSON 字符串存储,读取后必须重建 @Observed 装饰的类实例(而非直接赋值普通对象),否则 UI 无法响应数据变化;
    • 同步接口(如 getPreferencesSync/putSync)禁止在主线程处理大量数据(>1MB),避免触发 appfreeze 问题;
  4. 监听管理:页面/组件销毁时必须调用 offDataChange 取消数据监听,避免内存泄漏;
  5. 异常处理:所有异步操作(刷盘、删除文件)必须添加回调函数处理异常,JSON 解析需包裹 try/catch 防止数据损坏导致崩溃。

5.2 验证步骤

XML 模式验证
  1. 运行应用,输入用户名、切换主题模式,点击「保存用户信息」;
  2. 完全退出应用(不是后台挂起),重新启动应用,验证用户名和主题设置是否留存;
  3. 再次进入应用,修改信息但不点击保存,直接退出应用,验证数据是否未被持久化(XML 未刷盘)。

5.3 反序列化直接赋值普通对象:UI不刷新演示

反序列化普通对象不刷新

5.4 反序列化重建@Observed实例:UI正常刷新演示

反序列化重建对象监听正常刷新

5.5 GSKV 模式验证(API 18+)

  1. 修改 EntryAbility 初始化代码为:PreferencesManager.initWithContext(this.context, preferences.StorageType.GSKV)
  2. 运行应用,输入数据后直接退出,重新启动验证数据是否自动留存(无需手动刷盘);
  3. 多进程场景下(如应用包含多个 Ability),验证不同进程读写数据的一致性。

注意:切换存储模式测试时,需先卸载应用重新安装(或删除原有存储文件),否则初始化失败。

六 常见错误与解决方案

错误场景 原因分析 解决方案
重启后数据丢失(XML) 未调用 flush() 刷盘,数据仅存于内存 统一使用 putAndFlush/deleteAndFlush 方法,自动完成刷盘
GSKV 模式初始化失败 设备/系统版本不支持 GSKV 模式 通过 isStorageTypeSupported 判断,自动降级为 XML 模式
读取对象后 UI 不刷新 直接赋值普通对象,未重建 @Observed 实例 反序列化后通过 new User()/new UserSettings() 重建实例
监听内存泄漏 页面销毁未取消数据监听 aboutToDisappear 生命周期中调用 offDataChange
数据解析崩溃 JSON 数据损坏或格式错误 解析时添加 try/catch,解析失败时使用默认值兜底
主线程卡顿/ANR 主线程调用同步接口处理大量数据 耗时操作移至 Worker 线程执行,避免阻塞 UI 线程

七、仓库代码

  • 工程名称:PreferencesDemo
  • 仓库地址:https://gitee.com/HarmonyOS-UI-Basics/harmony-os-ui-basics.git

八、下节预告

下一节将针对本节单例模式工具类的局限展开升级与工程化封装:

  1. 重构核心架构:从「单文件单例」升级为「多文件实例池」,解决业务配置隔离、键名冲突问题;
  2. 工程化封装:将升级后的工具类解耦为独立鸿蒙静态库(HAR),实现跨项目复用;
  3. 增强核心能力:支持存储模式自动降级、静态+实例双调用、资源安全管理,适配企业级开发标准;
  4. 掌握HAR库的创建、打包、集成全流程,完成工具类从业务工程到通用库的落地。
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐