到底该把数据“塞哪儿”?——HarmonyOS 应用数据存储方式全对比(Preferences / RDBStore / 文件系统)
摘要: 本文对比鸿蒙开发中的三种数据存储方案:Preferences(轻量键值对)、RDBStore(关系型数据库)和文件系统,提供选型指南与实操示例。 Preferences:适合小规模配置(如主题、状态),简单高效,支持进程内监听。 RDBStore:处理结构化数据(如订单、笔记),支持SQL查询、事务和索引,适合复杂业务场景。 文件系统:存储大文件(如图片、日志),需自行管理读写安全与并发。
我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~
前言
直说吧:数据存哪儿决定了你的应用能跑多稳、扩展多快、踩坑多少。很多同学一上来就“能用就行”,久而久之就会变成“到处是 if-else 的存取逻辑”。这篇我把 Preferences(KV 配置)/ RDBStore(关系型数据库)/ 文件系统(file I/O) 摆在一张台面上,从能力特性 → 代码示例 → 典型场景 → 性能与安全 → 迁移建议,给你一套“先选型,再编码”的硬核指南。
1. 三种方式一眼明了
| 维度 | Preferences | RDBStore | 文件系统(File I/O) |
|---|---|---|---|
| 典型数据 | 轻量键值对、配置、状态位 | 结构化、多表关系、可查询 | 大对象、二进制、日志、缓存、导入导出 |
| 数据规模 | 小(几十 KB ~ 数 MB) | 中到大(MB~GB,视设备与表设计) | 任意(受磁盘与策略限制) |
| 读写模型 | KV:get/put/flush |
SQL:insert/query/update/delete、索引、事务 |
流式/随机读写:open/read/write |
| 事务能力 | 无(整体落盘) | 有(事务、索引、约束) | 无(需自管原子性) |
| 查询能力 | 无(只能按 key) | 强(多条件、聚合、排序、分页) | 无(需自建索引/目录结构) |
| 并发 & 锁 | 简单(进程内串行化) | 数据库层并发控制 | 需自己设计(文件锁/分片) |
| 备份迁移 | 简单(整体复制) | 结构化迁移(DDL/DML) | 取决于组织方式 |
| 场景标签 | “设置 / 最近状态 / 小缓存” | “业务数据 / 可检索 / 可回滚” | “媒体/附件/导出/缓存目录” |
金句:能用 Preferences 别上数据库;需要查询与约束就果断用 RDB;文件是存“大块头”的家。
2. Preferences(KV 配置)——轻快、省心、做“状态专家”
2.1 适用场景
- 用户偏好、开关、最近使用记录(如
theme=dark、lastTab=2) - 小体量缓存(例如计算结果、接口 ETag)
- 不追求复杂查询和强一致事务
2.2 代码速览(ArkTS)
import dataPreferences from '@ohos.data.preferences';
const PREF_NAME = 'app_prefs';
export async function prefs() {
const ctx = getContext(this); // 按你的模板获取
return await dataPreferences.getPreferences(ctx, PREF_NAME);
}
// 写入 & 刷新(提交到磁盘)
export async function setPref(key: string, value: any) {
const p = await prefs();
await p.put(key, value);
await p.flush();
}
export async function getPref<T = any>(key: string, def?: T): Promise<T> {
const p = await prefs();
const v = await p.get(key, def);
return v as T;
}
// 监听变化(进程内)
export async function onPrefChange(key: string, cb: (val: any) => void) {
const p = await prefs();
p.on('change', (changedKey) => {
if (changedKey === key) p.get(key, null).then(cb);
});
}
2.3 最佳实践
- 命名空间化:
ui.theme、user.<uid>.lastWorkspace,避免 key 冲突 - 批量写合并:多次
put后再flush(),减少 IO - JSON 值要控体量:大 JSON 请改用 RDB 或文件
- 可预置默认值:升级版本时给 key 设置安全默认
3. RDBStore(关系型数据库)——一旦涉及查询,就别硬撑
3.1 适用场景
- 结构化业务数据:用户、订单、聊天记录、笔记、标签等
- 复杂筛选/排序/分页/聚合
- 需要事务与约束(唯一键、外键,视实现支持)
3.2 代码速览(ArkTS)
import rdb from '@ohos.data.rdb';
const DB_NAME = 'app.db';
const VERSION = 1;
let store: rdb.RdbStore;
export async function openRdb(ctx: any) {
if (store) return store;
const config: rdb.StoreConfig = { name: DB_NAME, securityLevel: rdb.SecurityLevel.S1 };
store = await rdb.getRdbStore(ctx, config, VERSION, {
onCreate: async (db) => {
await db.executeSql(`
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
tags TEXT, -- JSON 存数组或做子表
createdAt INTEGER,
updatedAt INTEGER
);
CREATE INDEX IF NOT EXISTS idx_notes_updated ON notes(updatedAt DESC);
`);
},
onUpgrade: async (db, oldV, newV) => {
// 按版本做 DDL 迁移
if (oldV < 2) {
// await db.executeSql('ALTER TABLE notes ADD COLUMN ...');
}
},
onDowngrade: async (_db, _oldV, _newV) => {}
});
return store;
}
// CRUD
export async function insertNote(ctx: any, note: any) {
const db = await openRdb(ctx);
await db.insert('notes', note);
}
export async function queryNotes(ctx: any, q: { keyword?: string, limit?: number, offset?: number }) {
const db = await openRdb(ctx);
const predicates = new rdb.RdbPredicates('notes');
if (q.keyword) {
predicates.like('title', `%${q.keyword}%`);
}
predicates.orderByDesc('updatedAt').limit(q.limit ?? 20).offset(q.offset ?? 0);
const result = await db.query(predicates, ['id', 'title', 'updatedAt']);
const rows: any[] = [];
while (result.goToNextRow()) {
rows.push({
id: result.getString(result.getColumnIndex('id')),
title: result.getString(result.getColumnIndex('title')),
updatedAt: result.getLong(result.getColumnIndex('updatedAt')),
});
}
result.close();
return rows;
}
export async function txExample(ctx: any, ops: () => Promise<void>) {
const db = await openRdb(ctx);
try {
await db.beginTransaction();
await ops();
await db.commit();
} catch (e) {
await db.rollBack();
throw e;
}
}
3.3 最佳实践
- 索引要配齐:查询条件与排序字段建索引,避免全表扫描
- 防止 N+1:该冗余的适度冗余(或用视图/聚合表)
- 迁移脚本版本化:
onUpgrade细化到每个版本 - 大字段分离:富文本/二进制放文件,只在表里放路径或摘要
- 读写线程:批量写入放事务,查询尽量避免主线程阻塞
4. 文件系统(File I/O)——二进制的地盘、缓存的主场
4.1 适用场景
- 图片/视频/音频/文档等大对象
- 下载缓存、离线包、导出/导入
- 日志滚动、临时文件
4.2 代码速览(ArkTS)
import fs from '@ohos.file.fs';
import path from '@ohos.file.path';
export async function writeText(ctx: any, fileName: string, content: string) {
const dir = path.getFilesDir(ctx); // 应用私有目录
const fpath = `${dir}/${fileName}`;
const file = await fs.open(fpath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
await fs.write(file.fd, content);
await fs.close(file);
return fpath;
}
export async function readText(ctx: any, fileName: string) {
const dir = path.getFilesDir(ctx);
const fpath = `${dir}/${fileName}`;
const stat = await fs.stat(fpath);
const buf = new ArrayBuffer(stat.size);
const file = await fs.open(fpath, fs.OpenMode.READ_ONLY);
await fs.read(file.fd, buf);
await fs.close(file);
return String.fromCharCode.apply(null, Array.from(new Uint8Array(buf)));
}
// 流式写入大文件(下载)
export async function writeStream(ctx: any, fileName: string, stream: AsyncIterable<ArrayBuffer>) {
const dir = path.getCacheDir(ctx); // 缓存目录
const fpath = `${dir}/${fileName}`;
const file = await fs.open(fpath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
for await (const chunk of stream) {
await fs.write(file.fd, chunk);
}
await fs.close(file);
return fpath;
}
4.3 最佳实践
- 目录分层:
files/(持久) vscache/(可清理) vstemp/(临时) - 原子写:先写临时文件,再
rename覆盖,防止中途崩溃写坏 - 滚动日志:按大小/日期切片,避免单文件过大
- 清理策略:缓存加 LRU 或过期清理,避免长期膨胀
- 安全访问:外部共享用 SAF/媒体库接口,不要直接暴露私有路径
5. 选型决策树(1 分钟定方向)
-
只是开关/偏好/少量 KV? → Preferences
-
需要按条件查询/排序/分页/去重/事务? → RDBStore
-
存二进制/大对象/导入导出/缓存? → 文件系统
-
既有结构化又有大对象? → RDB + 文件(表里保存文件路径/哈希)
-
需要跨设备同步?
- 轻量状态:Preferences + 分布式 KV(另文)
- 业务数据:RDB 本地 + 同步层(增量日志/云)
6. 场景拆解与落地方案
6.1 设置与会话状态
- 方案:Preferences
- 要点:分模块命名,进程内监听变化,批量
flush
6.2 个人笔记/待办/聊天记录
- 方案:RDBStore
- 要点:主键(
id)、更新时间索引、分页查询、全文检索可加倒排(或简化为LIKE+ 辅助索引) - 附件:图片/语音放文件,RDB 保存路径与缩略信息
6.3 媒体缓存/离线包/大文件下载
- 方案:文件系统(
cache/) - 要点:断点续传、校验(MD5/SHA)、LRU 清理、原子覆盖
6.4 报表/导出导入
- 方案:文件系统 + RDB 导出器
- 要点:生成 CSV/JSON/自定义格式;导入时事务包裹,一致性校验
6.5 轻量“搜索历史/最近打开”
- 方案:Preferences(数组 JSON)或 RDB(需要去重/统计)
- 要点:按最近时间裁剪;超过阈值滚动清理
7. 性能、并发与可靠性
- 写入合并:Preferences 多次
put→ 单次flush();RDB 批量写 → 事务包裹 - 主线程友好:所有 IO 放到任务线程或异步,不阻塞 UI
- 索引与计划:RDB 频繁查询字段必须建索引;大查询分页(
LIMIT/OFFSET或游标) - 崩溃恢复:文件采用“临时写 + rename”;RDB 依赖事务与 WAL
- 容量控制:文件缓存设上限(如 200MB),定期清理;RDB 定期
VACUUM/REINDEX(视支持)
8. 安全与合规
- 隐私分级:敏感信息尽量不落盘;必须落盘 → 加密(应用侧加密后再写 Preferences/RDB/文件)
- 访问边界:私有目录优先;外部分享走系统分享/沙盒授权
- 备份策略:选定需要随备份迁移的数据(如 Preferences、RDB)与不需要的(缓存)
- 完整性:文件写入完成后校验哈希;RDB 关键表加校验字段或版本号
9. 迁移与演进(从“小作坊”到“工程化”)
-
KV → RDB:当 KV 中存了大量 JSON 且开始需要“查询条件”时,迁移到 RDB;
- 先在 RDB 建“影子表”,上线双写;
- 验证一致后停止写 KV,做一次性迁移脚本;
-
文件散乱 → 目录治理:制定固定层级(
module/date/hash),清理历史脏数据; -
RDB 结构升级:每次版本迭代只做向后兼容的变更,
onUpgrade中分步迁移; -
引入同步层:本地持久化不变,上面叠增量日志/回放/冲突合并(超纲就不展开了)。
10. 组合范例:笔记应用(RDB + 文件 + Preferences)
10.1 表结构
notes(id, title, contentPath, tagsJson, createdAt, updatedAt)- 文件:
files/notes/<id>.md - 偏好:
prefs.lastOpenedNoteId
10.2 操作流程
-
新建笔记:
- 先在
files/notes/<id>.md写入内容(临时 → rename) - RDB 插入记录,
contentPath指向文件
- 先在
-
打开笔记:
- RDB 查路径 → 读取文件内容
- 写
prefs.lastOpenedNoteId
-
搜索与列表:
- 全靠 RDB 查询(标题/更新时间/标签)
11. 常见坑位清单(别问我为什么这么熟)
- Preferences 里塞了几百 KB 的 JSON,启动
get卡顿 → 拆分/迁移 RDB - RDB 没索引,搜索一慢就全慢 → 建索引 + 分页
- 文件写到一半崩溃,重启打不开 → 原子写 + 校验
- 大图原图乱放,存储爆仓 → 缩略图分级 + LRU 清理
- 升级删字段,老用户崩溃 → onUpgrade 做兼容迁移
- 主线程做 IO → 全部改异步/后台任务
12. 速查:我该选哪个?
- 只想存“一个开关/一个值” → Preferences
- 要按条件查询/排序/分页 → RDBStore
- 要存图片/音频/离线包 → 文件系统
- 三者都有 → RDB(结构) + 文件(大对象) + Preferences(状态) 的“黄金三件套”
…
(未完待续)
更多推荐





所有评论(0)