25 — 数据持久化 Preferences 与状态恢复

一、引言

内存态状态(AppStorageV2、@Local、@Provider/@Consumer)随进程消亡而丢失。短视频应用中,"上次看到哪个视频、进度到哪里、停留在哪个页签"这类轻量配置如果每次启动都重置,体验会大打折扣。HarmonyOS 提供 @ohos.data.preferences 键值型持久化存储,以轻量、同步友好的方式解决这一问题。本文讲解 Preferences 的读写模型、批量操作与文件管理,并结合 multi-short-video 的场景给出"内存状态 ↔ 持久化"联动的落地设计。
在这里插入图片描述

二、Preferences 简介与适用场景

Preferences 以"键-值"对形式存储,值为 number、string、boolean、Array 等简单类型,适合保存用户偏好与轻量状态,不适合大数据与复杂查询。项目中的典型可持久化项:

存储项类型来源状态用途
videoIndexnumberAdaptiveVideo.curIndex恢复上次观看的视频下标
videoPositionnumberAdaptiveAVPlayer.currentProgress恢复播放位置
tabIndexnumberIndex.subTabIndex恢复底部页签
lastWidthBpstringWindowInfo.widthBp断点偏好记录

对比关系型数据库(RelationalStore):Preferences 适合"几十个键、简单读写";RDB 适合结构化、可查询、量大(如评论缓存)的数据。两者选型可归纳为:

维度PreferencesRelationalStore
数据模型键值对表 + 行 + 列
查询能力仅按 keySQL 谓词、排序、分页
数据规模少量轻量大量结构化
典型场景用户偏好、播放位置、页签索引评论缓存、作品列表、消息
复杂度高(建库建表、事务)

本项目评论、作品列表均为内存模拟数据,因此持久化层以 Preferences 为主即可;若后续引入离线评论缓存,再升级为 RDB。

三、Preferences 的读取与写入

获取 Preferences 实例需要上下文与文件名:

// 项目风格的 Preferences 封装(建议实现,仿照 WindowUtil 单例)
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';

const STORE_NAME = 'msv_user_config';

export class PreferenceUtil {
  private static instance: PreferenceUtil | undefined = undefined;
  private prefs?: preferences.Preferences;

  static getInstance(): PreferenceUtil {
    if (PreferenceUtil.instance === undefined) {
      PreferenceUtil.instance = new PreferenceUtil();
    }
    return PreferenceUtil.instance;
  }

  async init(context: common.UIAbilityContext): Promise<void> {
    this.prefs = await preferences.getPreferences(context, STORE_NAME);
  }

  async put(key: string, value: preferences.ValueType): Promise<void> {
    await this.prefs?.put(key, value);
    await this.prefs?.flush();   // 同步到磁盘,保证落盘
  }

  async get(key: string, defaultValue: preferences.ValueType): Promise<preferences.ValueType> {
    return await this.prefs?.get(key, defaultValue) ?? defaultValue;
  }
}

要点:getPreferences 以 (context, name) 创建/打开实例;put 修改内存缓存,flush() 才真正落盘;进程被杀前建议在 Ability 的 onBackground/onDestroy 中执行一次 flush,避免丢失。写入读取均为异步接口,注意用 await 串行化。

接口还支持回调风格与 Promise 风格两种调用方式,统一使用 Promise + async/await 更契合工程风格;getPreferences 返回的 Preferences 实例在应用退出后自动清理句柄,无需手动 close。此外,getAll() 返回的键值对象可直接用于状态恢复前的整体校验,避免逐键读取的开销。

四、批量操作与文件管理

频繁单键 flush 会触发多次 IO,批量场景应合并写入:

// 批量保存:一次 flush 提交多条
async savePlayState(index: number, position: number): Promise<void> {
  await this.prefs?.put('videoIndex', index);
  await this.prefs?.put('videoPosition', position);
  await this.prefs?.flush();   // 统一落盘一次
}

其他常用接口:getAll() 读取全部键值;delete(key) 删除单键;clear() 清空当前 Preferences 文件;on('change')/off('change') 监听文件变化(多实例场景同步)。每个应用可创建多个 Preferences 文件,建议按业务域划分(如 msv_user_config 存用户偏好、msv_play_state 存播放状态),避免单文件膨胀;文件过时可用 deletePreferences(context, name) 删除。

五、页面状态恢复实战:视频位置与页签

将持久化与 V2 状态管理联动,可实现"启动即恢复"。以视频页为例,约 500ms 的 timeUpdate 回调天然是"节流后的保存时机":

// 扩展示例:在 AdaptiveVideo.ets / AdaptiveAVPlayer.ets 中接入持久化(项目风格)
@ComponentV2
export struct AdaptiveAVPlayer {
  @Consumer('currentTime') currentProgress: number = 0;
  @Consumer('duration') duration: number = 0;

  aboutToAppear(): void {
    // 恢复上次观看位置
    PreferenceUtil.getInstance().get('videoIndex', 0).then((v) => {
      // 结合列表长度校验后回写父组件 curIndex
    });
  }

  private onTimeUpdateFunction: (updateTime: number) => void = (updateTime: number) => {
    if (this.currentIndex === this.index) {
      this.currentProgress = updateTime;
      PreferenceUtil.getInstance().put('videoPosition', updateTime); // 定时保存进度
    }
  }
}

恢复时须校验数据有效性(index 是否越界、position 是否小于 duration),再回写 @Param/@Consumer 状态;页签恢复同理,Index.ets 的 subTabIndex 在 aboutToAppear 中读取并赋值,即可保留"上次停留在我的页签"的体验。建议在 Ability 的 onBackground 回调中调用一次全局 flush,兜底异步落盘。

六、Preferences 的限制与选型

  1. 容量与规模限制:Preferences 适合少量键值,量大(千级以上)应改用 RDB。
  2. 类型限制:值为基本类型与数组,存对象需 JSON.stringify/parse;注意 ArkTS 的序列化约束。
  3. 异步模型:读写均异步,读取未完成时组件已渲染会导致短暂缺省,需要默认值兜底。
  4. 与 AppStorageV2 的分工:AppStorageV2 管"进程内全局内存态",Preferences 管"跨进程启动的持久态",二者通过启动时的"读→写回内存"和运行时的"内存→定时落盘"衔接,切勿把持久化逻辑散落在组件内。
  5. 版本与迁移:存储文件格式随版本演进需要兼容策略,建议在键名前加版本号或维护一份 schemaVersion 键,升级时据此做一次迁移写入,避免新旧版本字段不一致导致恢复异常。
  6. 写入时机与功耗:进度类高频写入会持续唤醒磁盘 IO,除按业务节流外,还可在页面可见性变化(onPageHide)与 Ability 退后台时兜底 flush,平衡可靠性与功耗。

七、总结与最佳实践

  1. 明确持久化边界:用户偏好、播放位置、页签索引这类轻量状态用 Preferences,结构化大数据用 RDB。
  2. 统一封装单例(PreferenceUtil),所有读写走封装接口,避免散落 getPreferences 调用。
  3. 批量写入 + 定时 flush:timeUpdate 高频回调中只 put 不频繁 flush,在 onBackground 统一落盘。
  4. 恢复状态必须校验(越界、过期),并在 aboutToAppear 阶段尽早回写内存状态。
  5. 键名常量化管理,按业务域拆分存储文件,配合 AppStorageV2 形成"内存 ↔ 磁盘"闭环。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐