👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
   我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 HarmonyOS 的过程都记录在这里。
  
  🛠️ 主要方向:ArkTS 语言基础、HarmonyOS 原生应用(Stage 模型、UIAbility/ServiceAbility)、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战,以及 Android → 鸿蒙的迁移踩坑与复盘。
  🧭 内容节奏:从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘,让每篇都有可落地的代码与方法论。
  💡 我相信:写作是把知识内化的过程,分享是让生态更繁荣的方式。
  
   如果你也想拥抱鸿蒙、热爱成长,欢迎关注我,一起交流进步!🚀

前言

做 HarmonyOS 应用时,数据“能不能存进去”通常不是最难的问题。

真正容易留下技术债的是:一个开关用了数据库,一组设置塞进 UI 状态持久化,业务数据又全部 JSON.stringify 后扔进 Preferences。功能刚开始都能工作,等到查询条件、数据规模、页面状态、跨设备能力逐渐复杂,才发现最初的数据模型已经选错了。

华为官方在数据存储方案说明中,把 PersistentStorage、用户首选项 Preferences、分布式键值数据库和关系型数据库放在一起进行了对比:它们解决的并不是同一层问题。PersistentStorage偏向 UI 状态持久化;Preferences面向 少量配置和用户偏好;KV Store适合 关系较弱、天然按键访问的业务数据;RDB则用于 具有明确字段、条件查询和关系模型的结构化业务数据。

这次不从概念表格开始,而是设计四种具体数据,各存一次:

  • 一个“是否显示提示卡片”的 UI 开关;
  • 一组字体大小、自动播放等用户偏好;
  • 一批按业务 ID 直接访问的草稿数据;
  • 一张需要条件查询的任务表。

看看四种存储为什么应该分开选。

HarmonyOS 7 已于 2026 年 8 月正式发布,对应 API 26。本文以当前 HarmonyOS 7 / API 26 官方文档为核验背景,但示例刻意只使用成熟的基础接口,不依赖 API 26 才新增的能力。

一、为什么“能存进去”不是选型标准

先看四份数据。

showGuideCard = true

fontScale = 1.1
autoPlay = false
language = "zh-CN"

draft/10001 = "{...}"
draft/10002 = "{...}"
draft/10003 = "{...}"

TASK
---------------------------------------------
id        title                done    priority
1         完成需求说明          0       2
2         检查接口文档          1       3
3         整理测试清单          0       1

如果只问“能不能保存”,Preferences 确实也可以保存 showGuideCard,KV 也可以把整张 TASK 表序列化成 JSON,RDB 更是什么都能往表里塞。

但这会掩盖三个真正应该先问的问题:

数据是什么结构?

是一个 UI 状态、几项独立配置、Key-Value,还是带列定义的记录集合?

平时怎么访问?

是页面绑定后自动变化、通过 key 精确读取,还是经常执行“查询所有未完成且 priority > 1 的任务”?

数据为什么需要持久化?

是为了应用重启后恢复 UI,保存用户选择,保留业务对象,还是支撑长期业务查询?

官方也明确建议从数据类型、访问模式、性能和扩展需求等维度选择存储方式,而不是只比较“能否持久化”。

二、先把四种能力的位置摆正

这四种方案实际上分成两层。

PersistentStorage 属于 ArkUI 状态管理体系。它通过 AppStorage 将选中的 UI 状态持久化,目的是让应用重新启动后仍然能够恢复对应状态。官方 FAQ 也明确指出,PersistentStorage 的持久化与读回 UI 都依赖 AppStorage。

Preferences、distributedKVStore 和 relationalStore 则属于 ArkData 数据管理能力。

当前 ArkTS 接口分别通过:

import { preferences } from '@kit.ArkData';
import { distributedKVStore } from '@kit.ArkData';
import { relationalStore } from '@kit.ArkData';

访问。

分布式 KV 模块首批接口从 API version 9 开始支持;当前文档仍提供 createKVManager()、getKVStore()、SingleKVStore.put() 等接口。

关系型数据库同样是 ArkData 的核心持久化方案,当前模块基于 SQLite 提供关系数据库管理能力,可以执行增、删、改、查以及 SQL。官方还特别提醒,大数据量查询应控制单次数据规模,并避免在主线程执行过重查询。

本文只讨论应用自己的本地数据,因此示例不涉及额外的跨设备同步流程,也不添加与场景无关的权限配置。

三、实验一:一个 UI 开关,用 PersistentStorage

第一个数据非常简单:

用户关闭一次首页提示卡片后,下次启动应用仍然保持关闭。

这个数据首先是 UI 状态,其次才是“需要保存”。

这里使用 PersistentStorage 更贴合它的语义。

这段代码解决的问题,就是让 showGuideCard 与 AppStorage 建立状态联系,并把该状态纳入持久化。

PersistentStorage.persistProp('showGuideCard', true);

@Entry
@Component
struct Index {
  @StorageLink('showGuideCard')
  showGuideCard: boolean = true;

  build() {
    Column({ space: 16 }) {
      if (this.showGuideCard) {
        Text('这里是一张首次使用提示卡片')
      }

      Button(this.showGuideCard ? '关闭提示' : '重新显示')
        .onClick(() => {
          this.showGuideCard = !this.showGuideCard;
        })
    }
    .width('100%')
    .padding(24)
  }
}

真正需要关注的不是 boolean 能否保存,而是数据有没有进入 UI 状态链路。

@StorageLink 与 AppStorage 对应属性建立双向同步,状态变化后 PersistentStorage 才有明确的持久化对象。

官方 FAQ 还指出一个非常容易误解的地方:PersistentStorage 依赖 AppStorage 的状态观察能力,复杂嵌套对象的深层属性修改不能简单理解为“一改对象内部任意字段就一定会被持久化”。官方示例中专门讨论了嵌套对象更新无法被观察的问题。

因此 PersistentStorage 很适合:

是否展示欢迎页
某个面板是否展开
当前选择的简单 UI 模式
需要重启恢复的少量页面状态

却不适合拿来充当通用业务数据库。

官方 FAQ 还建议避免持久化大型数据集和频繁变化的数据;PersistentStorage 的同步写入特征决定了它更应该保持“小而明确”。

四、实验二:一组用户偏好,用 Preferences

第二组数据是:

fontScale = 1.1
autoPlay = false
language = "zh-CN"

它们不是页面组件自己的状态,而是应用设置。

这时应该把“状态绑定”与“配置保存”分开。

官方对 Preferences 的定位就是保存应用配置信息。数据采用键值形式管理,适合少量用户设置,但由于数据会加载到内存中,因此并不适合存储大量业务数据。

根据当前官方 ArkTS API,可以从 ArkData 获取 Preferences 实例:

import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';

const PREF_NAME = 'user_settings';

async function saveUserPreferences(
  context: common.Context
): Promise<void> {
  const pref = await preferences.getPreferences(context, {
    name: PREF_NAME
  });

  await pref.put('fontScale', 1.1);
  await pref.put('autoPlay', false);
  await pref.put('language', 'zh-CN');

  await pref.flush();
}

读取时则按照 key 获取:

async function loadAutoPlay(
  context: common.Context
): Promise<boolean> {
  const pref = await preferences.getPreferences(context, {
    name: PREF_NAME
  });

  const value = await pref.get('autoPlay', false);
  return value as boolean;
}

这里 flush() 很关键。

put() 修改的是 Preferences 中的数据,持久化到文件则应按照 Preferences 接口约定执行刷盘操作。当前 HarmonyOS 文档仍提供 Preferences 及其持久化接口。

从设计上看,Preferences 最适合的是:

字体大小
主题选择
首页默认 Tab
开关设置
用户选择的语言
某个功能是否启用

如果开始出现几千条商品、聊天消息、离线记录,就应该停下来重新判断,而不是继续增加 Preference key。

五、实验三:一批 Key-Value 业务数据,用 KV Store

第三组数据稍微不一样。

假设应用允许保存编辑中的草稿,并且业务访问模式几乎始终是:

给我 draft/10001
保存 draft/10002
删除 draft/10003

数据之间没有复杂关系,也很少执行“按三个字段联合筛选”。

这种模型天然就是:

key -> value

官方对 distributedKVStore 的描述也是非关系型键值数据库:key 作为唯一标识,适合数据关系和业务关系较少的数据;同时它也是 HarmonyOS 分布式数据能力的一部分。

先创建 KVManager,再取得 SingleKVStore:

import { distributedKVStore } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';

async function openDraftStore(
  context: common.Context,
  bundleName: string
): Promise<distributedKVStore.SingleKVStore> {
  const manager = distributedKVStore.createKVManager({
    context: context,
    bundleName: bundleName
  });

  const options: distributedKVStore.Options = {
    createIfMissing: true,
    encrypt: false,
    backup: false,
    autoSync: false,
    kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION,
    securityLevel: distributedKVStore.SecurityLevel.S3
  };

  return await manager.getKVStore<distributedKVStore.SingleKVStore>(
    'draft_store',
    options
  );
}

拿到 SingleKVStore 后,业务操作反而非常直接:

async function saveDraft(
  store: distributedKVStore.SingleKVStore,
  draftId: string,
  content: string
): Promise<void> {
  await store.put(`draft/${draftId}`, content);
}

async function readDraft(
  store: distributedKVStore.SingleKVStore,
  draftId: string
): Promise<string> {
  const value = await store.get(`draft/${draftId}`);
  return value as string;
}

官方当前接口定义中,SingleKVStore.put() 支持 Uint8Array | string | number | boolean,并提供 Promise 形式的调用;KVStoreType.SINGLE_VERSION 则表示单版本 KV Store。

这里最容易产生一个误区:

看到“distributedKVStore”,就以为用了它就等于已经实现跨设备同步。

不是。

KV Store 本身提供分布式数据管理能力,但是否自动同步、使用哪种数据库类型,以及是否执行相应同步操作,是另外一层配置和业务设计。本文的 autoSync: false 明确把实验限制在本地键值存储,不把“选择 KV”偷换成“已经完成分布式业务”。

如果你的数据访问模式本来就是“知道 key,直接拿 value”,KV 会非常自然。

但如果需求逐渐变成:

找到 priority >= 2
并且 status = 0
并且 createdAt > 某个时间
然后按 createdAt 排序

再继续拼 key 就开始和数据模型对抗了。

六、实验四:一张真正的业务表,用 RDB

最后是任务数据:

id
title
done
priority
created_at

需求包括:

  • 查询所有未完成任务;
  • 按 priority 过滤;
  • 将某条任务更新为完成;
  • 后续还可能增加分类表、标签表。

这已经明显是关系型模型。

官方 relationalStore 基于 SQLite,为应用提供完整的关系数据库增删改查能力,同时可以直接执行 SQL。

下面先打开数据库并创建表。

import { relationalStore, ValuesBucket } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';

const DB_CONFIG: relationalStore.StoreConfig = {
  name: 'task.db',
  securityLevel: relationalStore.SecurityLevel.S1
};

const CREATE_TABLE =
  'CREATE TABLE IF NOT EXISTS TASK (' +
  'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +
  'title TEXT NOT NULL, ' +
  'done INTEGER NOT NULL, ' +
  'priority INTEGER NOT NULL, ' +
  'created_at INTEGER NOT NULL)';

async function openTaskDatabase(
  context: common.Context
): Promise<relationalStore.RdbStore> {
  const store =
    await relationalStore.getRdbStore(context, DB_CONFIG);

  await store.executeSql(CREATE_TABLE);
  return store;
}

写入一条任务:

async function insertTask(
  store: relationalStore.RdbStore,
  title: string,
  priority: number
): Promise<number> {
  const values: ValuesBucket = {
    title: title,
    done: 0,
    priority: priority,
    created_at: Date.now()
  };

  return await store.insert('TASK', values);
}

查询未完成任务时,则不需要自己设计诸如:

unfinished/high/10001
unfinished/low/10002

这样的复合 key。

可以直接建立谓词:

async function queryPendingTasks(
  store: relationalStore.RdbStore
): Promise<void> {
  const predicates =
    new relationalStore.RdbPredicates('TASK');

  predicates.equalTo('done', 0);

  const resultSet = await store.query(
    predicates,
    ['id', 'title', 'priority']
  );

  try {
    while (resultSet.goToNextRow()) {
      const id = resultSet.getLong(
        resultSet.getColumnIndex('id')
      );

      const title = resultSet.getString(
        resultSet.getColumnIndex('title')
      );

      const priority = resultSet.getLong(
        resultSet.getColumnIndex('priority')
      );

      console.info(
        `id=${id}, title=${title}, priority=${priority}`
      );
    }
  } finally {
    resultSet.close();
  }
}

这里真正重要的是 ResultSet 用完以后及时关闭,而不是让结果集长期占用数据库资源。

关系型数据库适合的也并不只是“数据很多”,关键在于:

数据具有稳定字段结构,并且需要按照字段条件进行查询、更新、排序或者建立关系。

RDB 当前接口还支持事务、谓词查询以及直接执行 SQL;HarmonyOS 7 当前 ArkData 文档继续维护 relationalStore,部分更高版本 API 还在扩展事务、LiteResultSet 等能力。

七、把四种数据放在一起,差别就明显了

实验数据选择核心原因典型访问方式
是否显示提示卡PersistentStorage本质是需要跨启动恢复的 UI 状态UI 双向绑定、状态改变
字号、自动播放、语言Preferences少量独立配置项按 key 读取/修改
draft/<id> 草稿KV Store业务关系弱,key 可以唯一定位对象按 key 增删改查
TASK 业务表RDB有字段结构,需要条件查询谓词、SQL、表级 CRUD

这张表其实比“哪个性能最好”更有用。

因为四者并不是一条从低级到高级的升级路线:

PersistentStorage
    ↓
Preferences
    ↓
KV
    ↓
RDB

这种理解并不准确。

更合理的判断方式是:

这是 UI 状态吗?
  └─ 是,而且需要应用重启恢复
       → PersistentStorage

这是少量用户配置吗?
  └─ 是
       → Preferences

这是天然 Key-Value 的业务数据吗?
  └─ 是,关系较弱
       → KV Store

需要字段查询、组合条件、事务或关系模型吗?
  └─ 是
       → RDB

华为官方的选型 FAQ 本质上也是按照数据形态和访问模式区分这些能力。

八、几个容易理解错的地方

1. PersistentStorage 不是“小型 Preferences”

两者虽然都能保存一个布尔值,但抽象层不一样。

PersistentStorage 服务的是 ArkUI 状态持久化,并依赖 AppStorage;Preferences 则是应用配置数据存储。

如果一个值和 UI 状态管理没有任何关系,仅仅是业务配置,用 Preferences 通常更符合数据职责。

2. Preferences 不是 JSON 数据库

把所有对象 JSON.stringify 后存进 Preferences,看起来很省事。

问题是这样会失去字段级查询和结构化操作能力。

数据一旦开始拥有明显业务生命周期,就应该重新判断它究竟是 KV 还是关系型数据。

3. KV 的优势不是“不用建表”

真正的判断依据应该是业务模型是否天然由 key 唯一定位。

如果为了实现:

状态 + 时间 + 用户 + 分类

不断设计复合 key,往往意味着这批数据更接近 RDB。

4. 用了 distributedKVStore 不等于已经完成跨设备业务

官方模块确实支持分布式 KV 数据管理,但同步模式、设备范围和冲突模型都属于额外设计问题。本文只验证本地 KV 数据模型,不把跨设备同步能力混进基础选型。

5. RDB 也不能不考虑数据规模

关系型数据库适合复杂查询,并不意味着一次查询多少数据都无所谓。

官方当前 relationalStore 文档建议,大数据量场景避免一次查询过多数据,并给出了单次查询不超过 5000 条、在 TaskPool 中执行较重查询等建议。

九、实际项目可以按这个顺序排查

如果已经有一个项目,不确定某份数据应该放哪里,可以按下面的顺序判断:

  1. 先看它是不是 UI 状态。 如果数据直接服务页面状态,而且目的就是跨启动恢复少量状态,再考虑 PersistentStorage。
  2. 再看是不是配置。 少量、相互独立、通常按 key 读取的设置项优先考虑 Preferences。
  3. 然后看数据关系。 业务对象之间关系很弱,而且已经存在天然唯一 key,可以考虑 KV Store。
  4. 最后看查询方式。 只要开始频繁出现多字段过滤、排序、批量更新、事务或者表关系,就应该认真考虑 RDB。
  5. 再核对数据量和变化频率。 不要把大量、频繁变化的数据塞进 PersistentStorage 或 Preferences。
  6. 需要跨设备时单独设计。 不要因为数据库名称里有“distributed”,就忽略同步模式、设备范围和冲突处理。

这个顺序有一个好处:先判断数据语义,再选 API,而不是先挑自己最熟悉的 API,再强行适配业务。

十、项目存储选型表

维度PersistentStoragePreferencesKV StoreRDB
所属体系ArkUI 状态管理ArkDataArkDataArkData
主要对象UI 状态用户偏好、应用配置键值业务数据结构化关系数据
数据模型AppStorage 属性Key-ValueKey-Value表、行、列
典型读取状态绑定指定 key指定 key / KV 查询谓词 / SQL
复杂条件查询不适合不适合不是主要优势适合
大量业务数据不适合不适合视模型而定更合适
UI 自动联动强需业务层同步 UI需业务层同步 UI需业务层同步 UI
关系模型无无弱关系强
典型案例展开状态、提示开关主题、字号、自动播放草稿、缓存业务对象消息、任务、订单等结构化记录
本文 HarmonyOS 7 使用方式当前 ArkUI 能力@kit.ArkData@kit.ArkData@kit.ArkData

开发经验总结

这四种方案真正的分界线,可以浓缩成四句话:

PersistentStorage 保存的是需要恢复的 UI 状态。

Preferences 保存的是少量用户偏好和应用配置。

KV Store 保存的是天然可以用 key 描述的弱关系业务数据。

RDB 管理的是需要字段、查询条件以及关系模型的结构化业务数据。

所以,下一次准备调用存储 API 时,可以先别写代码。

先把数据写在纸上,再问一句:

我以后到底准备怎样查询它?

很多存储选型问题,到这里其实已经有答案了。

如果一个原本只需要 get(key) 的数据,后来开始不断增加“按状态筛选、按时间排序、按用户统计”,通常不是继续设计新 key 的时候,而是重新检查数据模型的时候。

📝 写在最后

如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!

我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!

感谢你的阅读,我们下篇文章再见~👋

✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。

Logo

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

更多推荐