HarmonyOS APP开发中的系统偏好——从读写到迁移,一篇搞定偏好设置

举报
Jack20 发表于 2026/06/20 18:34:37 2026/06/20
【摘要】 HarmonyOS APP开发中的系统偏好:@ohos.data.preferences 全解析——从读写到迁移,一篇搞定偏好设置📌 核心要点:Preferences 是 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 时机——别忘,也别太勤

:只 putflush,应用崩溃后数据丢失。

:每次 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 等同步方法,可能造成卡顿。

:优先使用异步方法(getPreferencesputflush),同步方法(getStringSync 等)仅在确认数据量小且非频繁调用时使用。

4.4 多实例冲突

:同一文件创建多个 Preferences 实例,导致数据不一致。

:同一文件只创建一个实例,通过单例模式管理。如果确实需要多实例访问同一文件,务必在写入后及时 flush。

4.5 on(‘change’) 监听的数据类型

PreferenceChangeDatavalue 字段在 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 注意事项

  1. 加密偏好文件:HarmonyOS 6 支持对偏好文件设置加密密钥,但一旦设置,后续打开也必须传入相同密钥,否则无法读取数据。
  2. bigint 类型:新增的 bigint 类型支持大整数存储,但读取时需使用 getBigIntSync() 方法。
  3. 废弃 APIPreferenceChangeData.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 就像是应用的"记忆中枢"——轻量、高效、用起来顺手。掌握了它的读写、监听和迁移,你的应用就再也不会"失忆"了。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。