HarmonyOS 数据持久化选型:PersistentStorage、Preferences、KV、RDB 用四种数据做一次实验【鸿蒙心迹】

👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 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 中执行较重查询等建议。
九、实际项目可以按这个顺序排查
如果已经有一个项目,不确定某份数据应该放哪里,可以按下面的顺序判断:
- 先看它是不是 UI 状态。 如果数据直接服务页面状态,而且目的就是跨启动恢复少量状态,再考虑 PersistentStorage。
- 再看是不是配置。 少量、相互独立、通常按 key 读取的设置项优先考虑 Preferences。
- 然后看数据关系。 业务对象之间关系很弱,而且已经存在天然唯一 key,可以考虑 KV Store。
- 最后看查询方式。 只要开始频繁出现多字段过滤、排序、批量更新、事务或者表关系,就应该认真考虑 RDB。
- 再核对数据量和变化频率。 不要把大量、频繁变化的数据塞进 PersistentStorage 或 Preferences。
- 需要跨设备时单独设计。 不要因为数据库名称里有“distributed”,就忽略同步模式、设备范围和冲突处理。
这个顺序有一个好处:先判断数据语义,再选 API,而不是先挑自己最熟悉的 API,再强行适配业务。
十、项目存储选型表
| 维度 | PersistentStorage | Preferences | KV Store | RDB |
|---|---|---|---|---|
| 所属体系 | ArkUI 状态管理 | ArkData | ArkData | ArkData |
| 主要对象 | UI 状态 | 用户偏好、应用配置 | 键值业务数据 | 结构化关系数据 |
| 数据模型 | AppStorage 属性 | Key-Value | Key-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 的时候,而是重新检查数据模型的时候。
📝 写在最后
如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!
我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!
感谢你的阅读,我们下篇文章再见~👋
✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。
更多推荐



所有评论(0)