明明都是存个数据,为什么你的鸿蒙 App 越用越卡、还老丢东西?”——鸿蒙数据持久化全方案对比大解密
本文对比了鸿蒙系统的四种数据持久化方案: Preferences:轻量键值存储,适合配置类小数据(主题、登录态等),简单易用但不适合复杂数据。 文件存储:自由度高,适合大文本/二进制数据(缓存、日志等),但缺乏查询能力且需自行管理并发。 RDB:关系型数据库,支持SQL查询和事务,适合结构化业务数据(用户信息、订单等),但学习成本较高。 分布式数据库:实现多设备数据同步(备忘录、设置等),适合跨设
大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~
本文目录:
前言
先来一句实话:大多数应用一开始挂掉,不是因为功能写不完,而是因为数据管理一团乱。
今天你用 Preferences 存一坨配置,明天用文件随手写点 JSON,后天又糊弄了个 RDB 表……
等项目做大一点——
“这个 key 当时是我存的么?”
“这个 JSON 为啥有时候多字段,有时候少字段?”
“这张表是谁建的?字段命名是喝多了起的吗??”
更绝的是,鸿蒙给你准备的持久化方案,还不止一个:
- Preferences
- Data Storage / 文件存储
- RDB(关系型数据库)
- 分布式数据库(KV / 分布式 RDB 等)
每一个都能“完成任务”,但用错地方就会变成性能杀手/维护灾难。
这篇就来好好把这四个方案按在桌上,一个一个拎起来对比:
- Preferences:轻量键值对存储
- Data Storage(文件):文件级读写
- RDB:本地关系型数据库
- 分布式数据库:多设备同步的杀手锏
- 适用场景对比:到底啥场景选谁?
- 实战示例与性能优劣:真项目里怎么搭配使用最舒服?
不打官腔,有吐槽、有经验、有代码、有选型建议,还有踩坑血泪史。
看完你再选存储方案,就不会再是“感觉差不多就用这个吧”,而是——
“我很清楚,为什么这里用的是这个。” 😏
一、先搞清楚:鸿蒙里到底有几种持久化套路?
直接先上一个“全家福”的大概印象:
| 方案 | 典型形态 | 适合场景 |
|---|---|---|
| Preferences | 键值对,轻量存储 | 配置、开关、轻量用户信息、最近使用记录 |
| Data Storage / 文件 | 读写文件 | 大文本、JSON 缓存、日志、下载文件、图片/音视频 |
| RDB(关系数据库) | 数据库 + 表 + SQL | 本地业务数据(用户表、订单、消息)、复杂查询、多条件筛选 |
| 分布式数据库 | 分布式 KV / RDB | 多设备同步:备忘录、任务清单、聊天草稿、设置跨设备保持一致 |
忘掉“哪个更高级”这种说法,真正的判断标准只有一个:
“我的业务数据长什么样?怎么用?要支撑到什么规模?”
接下来我们按从最轻量到最硬核的顺序来拆。
二、Preferences:轻到不像数据库,但日常离不开它
2.1 它是干嘛的?
Preferences 就是典型的:“我就存几个 key-value,别给我上数据库那么重的东西。”
非常适合这几类数据:
- 用户偏好:
theme = dark、lang = zh-CN - 登录态:
token、userId(注意安全性) - 引导页是否展示:
hasShownGuide = true - 一些简单的“记住上一次选择”:
lastTab = 2
2.2 典型用法(ArkTS 伪代码示例)
以
@ohos.data.preferences为例,示意完整生命周期。
写入数据
import preferences from '@ohos.data.preferences';
import common from '@ohos.app.ability.common';
async function saveUserSettings(darkMode: boolean, language: string) {
const context = getContext(this) as common.UIAbilityContext;
const pref = await preferences.getPreferences(context, 'user_settings');
await pref.put('darkMode', darkMode);
await pref.put('language', language);
// 关键:flush 把数据真正写落盘
await pref.flush();
}
读取数据
async function loadUserSettings(): Promise<{ darkMode: boolean; language: string }> {
const context = getContext(this) as common.UIAbilityContext;
const pref = await preferences.getPreferences(context, 'user_settings');
const darkMode = await pref.get('darkMode', false);
const language = await pref.get('language', 'zh-CN');
return { darkMode, language };
}
删除 / 清空
await pref.delete('token');
await pref.flush();
// 或者完全清空
await pref.clear();
await pref.flush();
2.3 优点 ✅
- 接口简单,写起来毫无心理负担
- 轻量:适合配置类频繁读写
- 天然 key-value,拿来存“小数据点”极顺手
- 系统帮你管理文件位置,无需了解底层路径
2.4 缺点 ❌
- 不适合存大对象 / 列表 / 海量数据
- 不支持复杂查询:没有“WHERE / ORDER BY”
- 没有事务概念:不能保证多个 put 的事务一致性
- 滥用很容易变成“垃圾堆”,到处是神秘 key
2.5 一条血泪经验
千万不要拿 Preferences 当“简易数据库”用。
如果你开始往里塞:
- 100+ 个键
- 每个键对应一个大 JSON 字符串
- 字符串里有几十个字段
那你未来维护的时候,一定会想穿越回去给当时的自己一巴掌。
三、Data Storage / 文件:你想怎么存,我就怎么给你存
3.1 它到底是什么?
文件存储是所有平台的老朋友了:你拿到一个路径 / 句柄,随便读 / 随便写。
在鸿蒙里,典型通过 fileio 或媒体相关 API 来操作。
适合:
- 本地缓存(比如接口数据用 JSON 文件缓存)
- 大文本(日志、导出文件)
- 二进制数据(图片、音频、视频、下载资源)
- “不需要复杂查询,只是要读/写整块内容”的场景
3.2 ArkTS 简单示例:写一个 JSON 缓存
import fileio from '@ohos.fileio';
const CACHE_PATH = '/data/storage/el2/base/files/article_cache.json';
export async function saveArticleCache(data: object) {
const json = JSON.stringify(data);
const fd = fileio.openSync(CACHE_PATH, fileio.OpenMode.CREATE | fileio.OpenMode.TRUNC | fileio.OpenMode.READ_WRITE);
fileio.writeSync(fd, json);
fileio.closeSync(fd);
}
export async function readArticleCache<T>(): Promise<T | null> {
if (!fileio.accessSync(CACHE_PATH)) {
return null;
}
const fd = fileio.openSync(CACHE_PATH, fileio.OpenMode.READ_WRITE);
const buf = new ArrayBuffer(1024 * 1024); // 简单示例,实战中自己控制大小
const len = fileio.readSync(fd, buf);
fileio.closeSync(fd);
const json = String.fromCharCode(...new Uint8Array(buf, 0, len));
return JSON.parse(json) as T;
}
真实项目里当然需要更完善的 buffer 管理,这里只是个味道。
3.3 优点 ✅
-
完全自由:
- 想存 JSON、二进制、Protobuf 都行
-
不受“表结构”约束:非常适合半结构化 / 非结构化数据
-
适合大数据量:几十 MB 甚至上百 MB 很常见
-
与网络层、下载模块契合:下载到文件、断点续传等
3.4 缺点 ❌
- 你要自己管理文件名 / 目录结构
- 没有查询能力:想找某一条数据只能自己读出来过滤
- 没有事务,一半写完崩了就可能出现“写了一半”的脏数据
- 并发读写要自己注意锁 / 冲突
3.5 典型使用误区
误区:用一个 JSON 文件当数据库表用。
比如你搞了一个 todos.json:
[
{ "id": 1, "title": "买菜", "done": false },
{ "id": 2, "title": "写代码", "done": false },
...
// 1000+ 条
]
每次增删改都:
- 整个文件读进来
- 在内存里改
- 整个 JSON 再写回去
这样搞:
- 数据一多,读写都很慢
- 任何一个时刻崩了,容易导致文件半写坏掉
- 还没有事务,改两条的时候,只成功一条也不稀奇
总结一句:
文件非常适合“整体大块读写”,不适合高频、小粒度、带复杂条件的业务数据。
四、RDB:当你开始认真搞业务数据,就该请它出场
4.1 它是什么?
RDB = Relational DataBase,本质上是鸿蒙里的 SQLite 那一挂:
- 有“库”:一个
.db文件 - 有“表”:
user/order/message - 有“字段”:
id/name/created_at - 有“SQL”:
SELECT/INSERT/UPDATE/DELETE - 有“索引”:查询快不快全看你索引建得好不好
适合:
- 中大型业务数据:用户信息、本地消息、搜索历史、离线数据
- 需要多条件查询 / 排序 / 分页
- 希望“数据结构清晰、可维护”的场景
- 要保证事务一致性(比如同时插入多条记录,要么都成功,要么都失败)
4.2 建一个最典型的表:用户信息表
import relationalStore from '@ohos.data.relationalStore';
import common from '@ohos.app.ability.common';
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'app.db',
securityLevel: relationalStore.SecurityLevel.S1
};
let globalStore: relationalStore.RdbStore | undefined;
export async function getRdbStore(): Promise<relationalStore.RdbStore> {
if (globalStore) return globalStore;
const context = getContext(this) as common.UIAbilityContext;
return new Promise((resolve, reject) => {
relationalStore.getRdbStore(context, STORE_CONFIG, (err, store) => {
if (err) {
reject(err);
return;
}
globalStore = store;
resolve(store);
});
});
}
初始化表结构
async function initUserTable() {
const store = await getRdbStore();
await store.executeSql(`
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
age INTEGER,
created_at INTEGER
)
`);
}
插入、查询、更新示例
export async function insertUser(name: string, email: string, age: number) {
const store = await getRdbStore();
const valueBucket = {
name,
email,
age,
created_at: Date.now()
};
await store.insert('user', valueBucket);
}
export async function queryAdultUsers(): Promise<Array<{ id: number; name: string }>> {
const store = await getRdbStore();
return new Promise((resolve, reject) => {
store.querySql('SELECT id, name FROM user WHERE age >= ? ORDER BY created_at DESC', [18],
(err, resultSet) => {
if (err) {
reject(err);
return;
}
let res: Array<{ id: number; name: string }> = [];
while (resultSet.goToNextRow()) {
res.push({
id: resultSet.getLong(resultSet.getColumnIndex('id')),
name: resultSet.getString(resultSet.getColumnIndex('name'))
});
}
resultSet.close();
resolve(res);
});
});
}
export async function updateUserEmail(id: number, email: string) {
const store = await getRdbStore();
await store.executeSql('UPDATE user SET email = ? WHERE id = ?', [email, id]);
}
4.3 优点 ✅
- 结构化强:字段是什么就是什么,不会混来混去
- 支持复杂查询、多条件、排序、分页
- 支持事务(比如批量插入 / 修改)
- 性能好:合理建索引的话,几万行数据照样飞快
- 非常适合“核心业务数据”的本地持久化
4.4 缺点 ❌
- 初学者会嫌“麻烦”——要建表,要设计字段,要写 SQL
- 迭代表结构需要做迁移(版本管理)
- 简单项目刚起步的时候,用 RDB 可能觉得“有点重”
4.5 小结一句:
只要你在想:
“这些数据会越来越多,而且是业务的核心资产。”
那就老老实实用 RDB,
别再纠结“是不是可以用 Preferences/文件凑合一下”。
五、分布式数据库:多设备同步的灵魂玩法
5.1 它解决的到底是什么问题?
分布式数据库不是给你用来“取代 RDB 或 Preferences”的,而是解决一个完全不同维度的问题:
数据不再只属于“这台设备”,而是属于“这个用户 + 他的设备群”。
典型场景:
-
手机 + 平板 + PC 同一个账号登录:
- 备忘录、待办、收藏、设置自动同步
-
手表记录的步数、心率,出现在手机健康 App 里
-
TV 继续播放手机上刚看了一半的视频
在鸿蒙里,一般会遇到:
- 分布式 KV(Key-Value Store)
- 分布式 RDB(关系型 + 多端同步)
我们这里以“分布式 KV”来示意。
5.2 分布式 KV 简单示例
import distributedData from '@ohos.data.distributedData';
let kvManager: distributedData.KVManager | undefined;
let kvStore: distributedData.SingleKVStore | undefined;
async function getKvStore(): Promise<distributedData.SingleKVStore> {
if (kvStore) return kvStore;
const context = getContext(this) as any;
kvManager = distributedData.createKVManager({
context,
bundleName: 'com.example.app',
});
return new Promise((resolve, reject) => {
kvManager!.getKVStore(
{
storeId: 'note_store',
isAutoSync: true,
kvStoreType: distributedData.KVStoreType.SINGLE_VERSION
},
(err, store) => {
if (err) {
reject(err);
return;
}
kvStore = store;
resolve(store);
}
);
});
}
// 写入一条数据(会同步到其他设备)
export async function saveNote(id: string, content: string) {
const store = await getKvStore();
await store.put(id, content);
}
// 读取
export async function getNote(id: string): Promise<string | undefined> {
const store = await getKvStore();
return new Promise((resolve) => {
store.get(id, (err, data) => {
if (err) {
resolve(undefined);
return;
}
resolve(data as string);
});
});
}
// 监听变更(其他设备改了,我这边能收到)
export async function subscribeNoteChange(onChange: (id: string, value: string) => void) {
const store = await getKvStore();
store.on('dataChange', {
onChange: (change) => {
change.insertEntries.forEach(e => onChange(e.key, e.value as string));
change.updateEntries.forEach(e => onChange(e.key, e.value as string));
}
});
}
5.3 优点 ✅
- 多设备自动同步:你只写一份,系统帮你和其他设备对齐
- API 相对简单(如果你只做 KV)
- 和鸿蒙“多端协同”的理念高度契合
5.4 缺点 ❌
- 不适合存特别大的数据块
- 不适合搞复杂查询(KV 天然是 key → value)
- 需要考虑网络、同步时机、冲突处理等问题
- 对“同账号多设备”的场景比较友好,单设备纯离线的话略显浪费
六、适用场景对比:到底什么时候该选谁?
我们直接上一个“更实用”的对比表,尽量按你真会遇到的问题来排。
6.1 从“数据形态”角度看
| 问题 | 最推荐 | 次选 |
|---|---|---|
| 几个简单的开关 / 配置 | Preferences | 分布式 KV(需要多端同步时) |
| 大段 JSON 文本 / 缓存 | 文件 | RDB(如果后面要查) |
| 结构明确的业务数据(订单、用户) | RDB | 分布式 RDB |
| 图片 / 音频 / 视频 | 文件 / 媒体库 | 分布式 + 文件(看业务需要) |
| 多设备同步的小数据(设置、草稿) | 分布式 KV | RDB + 自己做同步 |
6.2 从“规模 + 性能”角度看
| 维度 | Preferences | 文件 | RDB | 分布式数据库 |
|---|---|---|---|---|
| 数据量(条数) | 几十(最多几百) | 看文件大小(MB 级) | 几千、几万、十几万都 OK | 视设计而定,一般偏中小 |
| 单条大小 | 小(字符串 / bool 等) | 小~中(KB~MB) | 行级数据 | 小(KB 级) |
| 查询复杂度 | O(1) by key | 需要自己读出+遍历 | SQL,WHERE/ORDER/LIMIT 随便搞 | 通常根据 key 或简单遍历 |
| 读写性能 | 非常快(轻量) | 取决于文件大小 | 很快(建索引后更快) | 本地快,同步取决于网络 |
| 事务一致性 | 无 | 无 | 有事务,支持事务块 | 一般有基本保证,需按文档理解 |
6.3 从“开发复杂度”角度看
| 指标 | Preferences | 文件 | RDB | 分布式数据库 |
|---|---|---|---|---|
| 上手难度 | ☆(最简单) | ☆☆ | ☆☆☆ | ☆☆☆ |
| 需要设计结构 | 低 | 自己约定 | 中(数据建模) | 中~高(还要考虑同步) |
| 迁移成本 | 中 | 中 | 中~高(表结构迁移) | 中~高 |
一句话:你花在 RDB 上的设计时间,会在维护期加倍赚回来。
而你在 Preferences / 文件上“省的那点时间”,可能会在后期 debug 时痛哭流涕地还回去。
七、实战:做一个小应用,四种方案一起上场
假设我们要做一个不算太小的鸿蒙 App:“多端同步待办 + 日志 + 设置”。
功能需求:
- 待办列表:标题、描述、是否完成、创建时间、优先级
- 用户设置:主题、语言、是否开启通知等
- 离线缓存:最近 50 条操作日志,支持导出为文件
- 多端同步:同一账号在手机 / 平板上看到同一份待办
我们看看如何组合这四种方案。
7.1 合理选型方案
-
待办列表(Todo)
- 本地:RDB
- 多端同步:分布式 KV 或 分布式 RDB
设计:
todo表:id,title,desc,done,priority,updated_at- 分布式部分可以只同步变更记录(比如通过 KV 存“增量变化”)
-
用户设置(Settings)
- 单设备:Preferences(本机设置)
- 多设备一致:分布式 KV + 本地 Preferences 做缓存
-
操作日志(Logs)
- 用文件存储:每天一份
log_YYYYMMDD.txt或统一app.log - 支持一键导出/上传
- 用文件存储:每天一份
-
临时搜索结果 / 缓存列表
- 可以用文件 + 内存缓存
7.2 简化版数据层代码结构
/service
todo-rdb.ets # 用 RDB 管理待办核心数据
todo-sync.ets # 用分布式 KV 做多端同步
settings-pref.ets # 用户设置:Preferences 封装
settings-distributed.ets # 多端同步设置(可选)
log-file.ets # 日志写文件
待办 RDB:插入 + 查询
// todo-rdb.ets
export async function addTodo(item: TodoItem) {
const store = await getRdbStore();
await store.insert('todo', {
title: item.title,
desc: item.desc,
done: item.done ? 1 : 0,
priority: item.priority,
updated_at: Date.now()
});
}
export async function listTodo(): Promise<TodoItem[]> {
const store = await getRdbStore();
return new Promise((resolve, reject) => {
store.querySql('SELECT * FROM todo ORDER BY updated_at DESC', [],
(err, resultSet) => {
if (err) {
reject(err);
return;
}
let list: TodoItem[] = [];
while (resultSet.goToNextRow()) {
list.push({
id: resultSet.getLong(resultSet.getColumnIndex('id')),
title: resultSet.getString(resultSet.getColumnIndex('title')),
desc: resultSet.getString(resultSet.getColumnIndex('desc')),
done: resultSet.getLong(resultSet.getColumnIndex('done')) === 1,
priority: resultSet.getLong(resultSet.getColumnIndex('priority')),
updatedAt: resultSet.getLong(resultSet.getColumnIndex('updated_at')),
});
}
resultSet.close();
resolve(list);
});
});
}
设置:Preferences + 分布式 KV 双写
// settings-pref.ets
export async function saveLocalSettings(settings: AppSettings) {
const context = getContext(this) as common.UIAbilityContext;
const pref = await preferences.getPreferences(context, 'settings');
await pref.put('theme', settings.theme);
await pref.put('lang', settings.lang);
await pref.put('notify', settings.notify);
await pref.flush();
}
// settings-distributed.ets
export async function syncSettingsToCloud(settings: AppSettings) {
const store = await getKvStore();
await store.put('settings', JSON.stringify(settings));
}
export async function loadSettings(): Promise<AppSettings> {
// 1. 优先用本地 Preferences
// 2. 若本地为空,尝试从分布式 KV 获取后落地
}
日志文件:简单到爆,但非常好用
// log-file.ets
const LOG_PATH = '/data/storage/el2/base/files/app.log';
export function appendLog(line: string) {
const fd = fileio.openSync(LOG_PATH,
fileio.OpenMode.CREATE | fileio.OpenMode.READ_WRITE | fileio.OpenMode.APPEND);
const content = `[${new Date().toISOString()}] ${line}\n`;
fileio.writeSync(fd, content);
fileio.closeSync(fd);
}
7.3 如果你选错,会怎样?(真实翻车剧本)
错误玩法 1:把所有待办都存 Preferences
await pref.put('todo_list', JSON.stringify(bigArray));
后果:
- 每次增删改都得读整个数组 → 改 → 写回
- 待办一多,首屏加载会变慢得肉眼可见
- 你还不能做复杂筛选(比如“优先级=高 且 未完成 且 最近 7 天”)
错误玩法 2:把用户设置写进 RDB
虽然“功能上没毛病”,但:
- 对读取这种小配置来说,RDB 有点太重
- 多一个 service,多几层封装
- Preferences 解决的问题非要让 RDB 来解决,其实是在浪费时间
错误玩法 3:完全不考虑分布式
假设你的 App 是“多设备协同待办”,结果你所有数据只存本地 RDB:
- 手机上勾选了“已完成”,平板上还是“未完成”
- 用户会以为:“这同步做坏了”
- 你再补同步方案,就得做一堆复杂补丁
正确姿势:
核心数据仍用 RDB 管本地一致性,“改变记录”或最终快照用分布式 KV 同步出去,多端收到后再更新各自 RDB。
八、性能与选型:一套“简单粗暴但十分好用”的决策树
最后给你一张“脑子累了时用的图”:
每次你要做数据持久化的时候,就按下面几步问自己。
Step 1:数据是不是跨设备要保持一致?
-
是 → 优先考虑“分布式数据库”
- 小数据/配置:分布式 KV
- 结构复杂:分布式 RDB + 本地缓存
-
否 → 看下一步
Step 2:是不是结构化业务数据?会不会越来越多?
- 是 → RDB,乖乖设计表
- 否 → 看下一步
Step 3:是不是简单配置/状态?
- 是 → Preferences
- 否 → 看下一步
Step 4:是不是偏文件/缓存性质的数据?
- 文本、JSON、大对象、下载、媒体
→ 文件存储
结语:数据持久化选型,决定的是“你未来会不会被自己写的东西拖死”
很多人刚开始做 App,心态大概是这样的:
“先实现功能,数据这块先随便搞搞,以后再重构。”
然后你会发现:以后永远不会自己来。
等到你真想重构时,项目已经到处都是:
- 随手命名的 Preferences key
- 到底哪个 JSON 里有哪个字段谁也说不清
- 神秘的
.db文件里有一堆“为了赶工随便建的表”
而如果你在一开始,就对这四种方案有一个明确认知:
- Preferences → 小配置、小状态
- 文件 → 大块数据、缓存、日志
- RDB → 业务核心、本地表结构
- 分布式数据库 → 多端协同、账号级数据
你会发现:
- 数据越来越多时,代码结构却越来越稳
- 新需求来了,不是“完了我得重写一遍”,而是“在哪一层加一点东西就行”
- 你的 App 不只是“能跑”,而是“跑得稳、跑得久、跑得起规模”
数据,是应用的“骨头”。
你想做个有战斗力的鸿蒙应用,先把骨架打好。
如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~
更多推荐




所有评论(0)