HarmonyOS 5 数据持久化:状态持久化 (PersistentStorage)

大家好,我是不想掉发的鸿蒙开发工程师 城中的雾。在上一期中,我们学会了用首选项(Preferences)手动存取数据。虽然封装了工具类,但每次修改数据还得调用 PreferenceUtil.put,是不是还是觉得有点麻烦,有没有一种办法,能让我像操作普通变量一样:

this.myVar = 100;

然后系统就自动把它存进硬盘;下次打开 App,变量自动变回 100?有!这就是鸿蒙的 PersistentStorage。今天我们来聊聊这个状态变量持久化。

1. 它是谁?AppStorage 的“影子”

要理解 PersistentStorage,必须先理解 AppStorage

  • AppStorage:是应用全局的内存状态管理器。数据存在内存里,杀进程就没了。
  • PersistentStorage:是 AppStorage 的持久化“插件”。它将磁盘文件与 AppStorage 中的特定属性建立双向绑定。

工作原理 (双向绑定)

UI组件 (@StorageLink)  <==>  AppStorage (内存)  <==>  PersistentStorage (磁盘)

当你修改 UI 绑定的 @StorageLink 变量时 -> AppStorage 更新 -> PersistentStorage 自动写入磁盘。

当你重启 App 时 -> PersistentStorage 读取磁盘 -> 填充 AppStorage -> UI 自动显示旧数据。

2. 核心 API:persistProp

使用 PersistentStorage 极其简单,核心方法只有一个:persistProp

// 将 'is_night_mode' 属性持久化,默认值为 false
// 这行代码必须在 UI 使用该属性之前调用!
PersistentStorage.persistProp('is_night_mode', false);

执行流程

  1. 检查磁盘里有没有 is_night_mode
  2. :把磁盘里的值读出来,覆盖到 AppStorage 中。
  3. 没有:把默认值 false 写入 AppStorage。

3. 实战:搜索历史记录

我们来实现一个经典的“搜索历史”功能。用户输入关键词搜索,历史记录自动保存;杀掉 App 再打开,历史记录还在。

第一步:初始化 (EntryAbility)

关键点persistProp 最好放在 EntryAbilityonWindowStageCreate 中,确保初始化成功,持久化链接已经建立。

// EntryAbility.ets
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
  }
    onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
    windowClass = windowStage.getMainWindowSync();
    windowStage2 = windowStage
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
        return;
      }
        // 初始化持久化属性
    	// 'search_history' 是 key,[] 是默认值
      PersistentStorage.persistProp('search_history', []);
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
    });
  }
}

第二步:UI 实现 (SearchPage.ets)

在页面中,我们只需要使用 @StorageLink 装饰器。完全不需要写任何“保存”或“读取”的代码!

在这里插入图片描述

@Entry
@Component
struct SearchPage {
  @State inputText: string = '';
  
  // 核心
  // 使用 @StorageLink 双向绑定全局状态
  // 当 this.history 发生变化时,UI 会刷新,且 PersistentStorage 会自动将其写入磁盘
  @StorageLink('search_history') history: string[] = [];

  build() {
    Column() {
      // 搜索框区域
      Row() {
        TextInput({ placeholder: '请输入搜索内容', text: this.inputText })
          .layoutWeight(1)
          .onChange((val) => this.inputText = val)
          .onSubmit(() => this.doSearch()) // 回车搜索
        
        Button('搜索')
          .onClick(() => this.doSearch())
          .margin({ left: 10 })
      }
      .width('100%')
      .padding(10)
      .backgroundColor('#F0F0F0')

      // 历史记录标题
      Row() {
        Text('历史记录').fontSize(14).fontColor('#999')
        Blank()
        // 清空按钮
        Text('清空')
          .fontSize(14)
          .fontColor('blue')
          .onClick(() => {
            // 直接清空数组,磁盘也会自动清空!
            this.history = []; 
          })
      }
      .width('100%')
      .padding(10)

      // 历史列表
      List() {
        ForEach(this.history, (item: string) => {
          ListItem() {
            Text(item)
              .fontSize(16)
              .padding(10)
              .width('100%')
              .border({ width: { bottom: 1 }, color: '#EEE' })
          }
        })
      }
      .layoutWeight(1)
      .width('100%')
    }
    .height('100%')
  }

  // 执行搜索逻辑
  doSearch() {
    if (this.inputText.trim() === '') return;
    
    // 1. 简单的去重逻辑:如果存在先删除
    const index = this.history.indexOf(this.inputText);
    if (index !== -1) {
      this.history.splice(index, 1);
    }
    
    // 2. 插入到头部
    this.history.unshift(this.inputText);
    
    // 3. 限制长度(比如只存 10 条)
    if (this.history.length > 10) {
      this.history.pop();
    }
    
    // 清空输入框
    this.inputText = '';
    
    // 【注意】:对于数组和对象,如果是通过方法修改内部元素(如 push/splice),
    // 必须确保 @StorageLink 能感知到。
    // 在 ArkUI 中,Array 的方法通常会被拦截并触发更新,
    // 但为了保险,有时需要重新赋值:this.history = [...this.history];
    // 不过在 @StorageLink 中,直接调用变异方法通常是生效的。
  }
}

体验效果

  1. 输入 “鸿蒙”,点击搜索。列表出现 “鸿蒙”。
  2. 输入 “ArkTS”,点击搜索。列表出现 “ArkTS”, “鸿蒙”。
  3. 强杀 App 进程
  4. 重新打开 App。你会发现列表里依然躺着 “ArkTS” 和 “鸿蒙”。

全程没有调用过一次 saveflush,调用存储也很便捷

4. 避坑指南:魔法的代价

虽然 PersistentStorage 很好用,但它不是万能的,甚至有很多“坑”。

坑 1:不要存复杂对象

它支持 number, string, boolean, enum 等基础类型。

如果你存一个复杂的 class 实例或者嵌套很深的 JSON:

  • 序列化性能差:每次修改都会触发 JSON 序列化和磁盘写入,导致 UI 掉帧。
  • 状态丢失:存进去的是 Object,读出来可能就丢失了类的方法(Prototype)。

坑 2:不要绑定高频变化的数据

错误示范

// 监听滚动位置
@StorageLink('scroll_y') scrollY: number = 0;

onScroll((y) => {
  this.scrollY = y; // 每一帧都在疯狂写磁盘!!手机会发烫卡顿。
})

坑 3:初始化顺序

一定要先调用 PersistentStorage.persistProp,再使用 @StorageLink。

如果在 persistProp 之前页面就已经加载并使用了 @StorageLink,那么页面可能会显示默认值,而磁盘里的旧数据会被覆盖或忽略。

最佳实践:始终在 EntryAbility.onWindowStageCreate里做持久化初始化。

坑 4:PersistentStorage vs Preferences

特性 首选项 (Preferences) 状态持久化 (PersistentStorage)
操作方式 手动 (get/put) 自动 (绑定变量)
适用场景 逻辑配置、非 UI 强关联数据 UI 状态、用户输入历史、设置开关
性能 较好 (手动控制刷盘) 一般 (变化即刷盘)
灵活性 高 (可在 Utils 使用) 低 (依赖 AppStorage)

5. 总结

  • PersistentStorage 是 AppStorage 的持久化扩展,实现了内存与磁盘的双向绑定
  • 使用 @StorageLink 可以让你像操作普通变量一样操作磁盘数据。
  • 切记:不要存大数据,不要存高频变化的数据,一定要在 Ability 中尽早初始化。

下一期预告:

如果我要存 1000 条聊天记录,或者要存一个包含“姓名、年龄、头像、简介”的用户列表,首选项和 PersistentStorage 都不好使了(性能太差,且无法查询)。

这时候,我们需要使用——关系型数据库 (RelationalStore)。

下一篇,我们在手机里塞个 Excel,学学 SQL 怎么写。

📚 充电时间

如果有想加入鸿蒙生态的大佬们,快来加入鸿蒙认证吧!初高级证书还没获取的,点这里:

🔗 HarmonyOS第一课:官方认证培训

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信中提出,非常感谢您的支持。

Logo

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

更多推荐