【鸿蒙】HarmonyOS 数据持久化:Preferences/KV Store/RelationalStore 选型指南

HarmonyOS 数据持久化:Preferences/KV Store/RelationalStore 选型指南

> 读完本文,你将掌握三种持久化 API 的核心差异、选型决策树,以及常见坑点的规避方法,不再因选错存储方案而踩坑。

> 适用版本:HarmonyOS NEXT / API 12+ | ⏱ 阅读约 18 分钟


背景:你真的需要数据库吗?

很多开发者在需要"保存一个布尔值"时直接引入关系型数据库,又在需要"存储用户行为日志"时用 Preferences。选型错误带来的不只是性能损耗,还有数据一致性风险。

HarmonyOS NEXT 提供三条数据持久化路径:

复制代码
应用数据持久化 API

├── Preferences(用户首选项)


│     └── 轻量 KV,XML 序列化,适合配置项


├── KV Store(分布式键值存储)


│     └── 跨设备同步,适合轻量共享状态


└── RelationalStore(关系型数据库)


└── SQLite 封装,适合结构化/大量数据

一、Preferences:用户配置的最优解

1.1 内部机制

Preferences 将数据序列化为 XML 文件存储在应用沙箱,整个文件在首次访问时全量加载进内存 ,后续读写操作均在内存中完成,调用 flush() 才落盘。

复制代码
Preferences 读写流程:

┌──────────┐   getPreferences()   ┌──────────────┐


│ Context  │ ──────────────────▶  │ 内存 Map│


└──────────┘                      └──────┬───────┘


│ flush() / 进程退出


┌─────▼──────┐


│  .xml 文件  │


└────────────┘

1.2 基本用法

复制代码
import { preferences } from '@kit.ArkData';

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



// ✅ 正确:在 UIAbility 中获取 context 后使用


const PREF_NAME = 'user_settings';



async function saveTheme(context: common.UIAbilityContext, isDark: boolean) {


// 获取 Preferences 实例(同名文件只会加载一次)


const pref = await preferences.getPreferencesSync(context, { name: PREF_NAME });


pref.putSync('isDarkMode', isDark);  // 写入内存


await pref.flush();                  // 必须 flush 才落盘!


}



async function readTheme(context: common.UIAbilityContext): Promise {


const pref = await preferences.getPreferencesSync(context, { name: PREF_NAME });


return pref.getSync('isDarkMode', false) as boolean; // 第二参数为默认值


}

错误写法 → 问题 → 正确写法

| 错误写法 | 问题 | 正确写法 |

|---------|------|---------|

| pref.put('key', val) 后不调用 flush() | 应用闪退时数据丢失 | 每次写入后调用 await pref.flush() |

| 在 @Entry 组件 build() 中调用 getPreferences() | 每次渲染都重建实例,内存浪费 | 在 aboutToAppear() 或 Service 层中统一初始化 |

| 用 Preferences 存储超过 8KB 的字符串 | XML 解析变慢,全量加载内存压力大 | 超过 8KB 的数据改用 RelationalStore 或 KV Store |

1.3 适用场景

  • 用户偏好设置(主题、语言、字体大小)

  • 开关类配置(通知开启状态、是否首次启动)

  • 轻量数值(上次浏览位置、计数器)
    不适用: 超过 100 个 key、单 value 超 8KB、需要多线程并发写入。


二、KV Store:分布式场景的首选

2.1 架构概览

KV Store 基于 HarmonyOS 分布式数据服务(DDS),本地写入会通过可信通道同步到同账号下的其他设备。

复制代码
KV Store 数据流:

手机 App                  平板 App


┌──────────┐   同步通道    ┌──────────┐


│ KVManager│ ◀──────────▶ │ KVManager│


│  Local DB│              │  Local DB│


└────┬─────┘              └──────────┘


│ 本地持久化


┌──▼──────┐


│RocksDB  │  ← 底层存储引擎


└─────────┘

2.2 初始化与读写

复制代码
import { distributedKVStore } from '@kit.ArkData';


class KVStoreManager {


private kvManager: distributedKVStore.KVManager | null = null;


private kvStore: distributedKVStore.SingleKVStore | null = null;



async init(context: Context) {


const config: distributedKVStore.KVManagerConfig = {


context,


bundleName: 'com.example.myapp',


};


this.kvManager = distributedKVStore.createKVManager(config);



// SINGLE_VERSION:单设备版本,不冲突;DEVICE_COLLABORATION:多设备协同


const options: distributedKVStore.Options = {


createIfMissing: true,


encrypt: false,


backup: false,


autoSync: true,                                    // 自动同步到同账号设备


kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION,


schema: undefined,


securityLevel: distributedKVStore.SecurityLevel.S1,


};


this.kvStore = await this.kvManager.getKVStore(


'my_store_id', options


);


}



async put(key: string, value: string) {


await this.kvStore!.put(key, value);


}



async get(key: string): Promise {


const data = await this.kvStore!.get(key);


return data as string;


}


}

错误写法 → 问题 → 正确写法

| 错误写法 | 问题 | 正确写法 |

|---------|------|---------|

| 每次操作都调用 getKVStore() | 频繁创建销毁,性能差 | 单例初始化,App 生命周期内复用 |

| autoSync: true + 高频写入 | 流量消耗大,影响电量 | 高频场景设 autoSync: false,批量完成后手动 sync() |

| 不监听 DATA_CHANGE 事件 | 跨设备更新后 UI 不刷新 | 注册 on('dataChange', ...) 回调更新状态 |


三、RelationalStore:结构化数据的唯一选择

3.1 核心能力

RelationalStore 是对 SQLite 的封装,支持事务、索引、复杂 WHERE 查询,提供 ValuesBucket/RdbPredicates 等 ArkTS 友好 API。

复制代码
RelationalStore 调用链:

┌──────────────┐


│ RdbPredicates│  ← 类型安全的条件构建器


└──────┬───────┘


│ equalTo / between / orderByDesc ...


┌──────▼───────┐   SQL 翻译   ┌────────┐


│  RdbStore    │ ────────────▶│ SQLite │


└──────┬───────┘              └────────┘


│ ResultSet


┌──────▼───────┐


│  数据游标    │  ← 惰性加载,不一次性拷贝全部数据


└──────────────┘

3.2 完整 CRUD 示例

复制代码
import { relationalStore } from '@kit.ArkData';


const DB_CONFIG: relationalStore.StoreConfig = {


name: 'notes.db',


securityLevel: relationalStore.SecurityLevel.S1,


};



const CREATE_TABLE = `


CREATE TABLE IF NOT EXISTS notes (


id    INTEGER PRIMARY KEY AUTOINCREMENT,


title TEXT    NOT NULL,


body  TEXT,


ts    INTEGER DEFAULT (strftime('%s','now'))


)`;



let rdbStore: relationalStore.RdbStore | null = null;



async function initDB(context: Context) {


rdbStore = await relationalStore.getRdbStore(context, DB_CONFIG);


await rdbStore.executeSql(CREATE_TABLE);


}



async function insertNote(title: string, body: string): Promise {


const bucket: relationalStore.ValuesBucket = { title, body };


return await rdbStore!.insert('notes', bucket);


}



async function queryNotes(keyword: string) {


const predicates = new relationalStore.RdbPredicates('notes');


predicates.contains('title', keyword)


.orderByDesc('ts')


.limitAs(20);



const cursor = await rdbStore!.query(predicates, ['id', 'title', 'body', 'ts']);


const results: Array<{ id: number; title: string }> = [];



while (cursor.goToNextRow()) {


results.push({


id: cursor.getLong(cursor.getColumnIndex('id')),


title: cursor.getString(cursor.getColumnIndex('title')),


});


}


cursor.close(); // ⚠️ 必须手动 close,否则 SQLite 连接泄漏


return results;


}



async function batchInsert(notes: Array<{ title: string; body: string }>) {


await rdbStore!.beginTransaction();


try {


for (const n of notes) {


await rdbStore!.insert('notes', { title: n.title, body: n.body });


}


await rdbStore!.commit();


} catch (e) {


await rdbStore!.rollBack();


throw e;


}


}

错误写法 → 问题 → 正确写法

| 错误写法 | 问题 | 正确写法 |

|---------|------|---------|

| 每次读写都 getRdbStore() | SQLite 连接建立代价大,频繁开关严重拖慢速度 | AbilityStage 中单例初始化,全局复用 |

| cursor.query() 后不 cursor.close() | 连接未释放,并发查询超出上限后 crash | 使用 try-finally 保证 cursor.close() 执行 |

| 不使用事务批量插入 1000 条 | 每条独立事务,速度降低约 50 倍 | 用 beginTransaction/commit 包裹批量操作 |

| 手写 SQL 字符串拼接用户输入 | SQL 注入风险 | 始终使用 RdbPredicates 构建条件 |


四、选型决策树

复制代码
需要持久化数据?

├── 是跨设备同步数据?


│     └── YES → KV Store(autoSync: true)


│     └── NO ↓


├── 数据量 < 100 条 且 结构简单(KV 对)?


│     └── YES → Preferences


│     └── NO ↓


└── 需要结构化查询 / 数据量大 / 需要事务?


└── YES → RelationalStore(SQLite)

| 维度 | Preferences | KV Store | RelationalStore |

|------|-------------|----------|-----------------|

| 数据结构 | 扁平 KV | 扁平 KV | 关系型表 |

| 存储引擎 | XML 文件 | RocksDB | SQLite |

| 跨设备同步 | 不支持 | 原生支持 | 不支持 |

| 事务支持 | 不支持 | 不支持 | 支持 |

| 最大数据量 | < 100 个 key | 中等 | 理论无上限 |

| 查询能力 | 按 key 精确 | 按 key 精确 | SQL 全功能 |


五、最佳实践

5.1 统一在 AbilityStage 中初始化存储

做法:AbilityStage.onCreate() 中初始化所有持久化实例,注入到 AppStorage。 原因: AbilityStage 早于 UIAbility 启动,确保 UI 层使用时实例已就绪,避免空指针异常。 对比: 若在 @Entry 组件的 aboutToAppear() 中初始化,多个页面会重复创建实例,且页面切换时可能出现短暂不可用窗口。

5.2 Preferences 的 flush 策略

做法: 普通配置项写入后调用 flush();高频临时状态在 UIAbility.onBackground() 中统一落盘。 原因: flush() 是磁盘 I/O,频繁调用导致 UI 卡顿或过多 I/O 唤醒耗电。 对比: 每次写都 flush() 会使 60fps 滚动掉帧;完全不 flush() 则进程被杀后丢数据。

5.3 RelationalStore 必须使用索引

做法: 对 WHERE/ORDER BY 字段建索引:CREATE INDEX IF NOT EXISTS idx_ts ON notes(ts)原因: 无索引的全表扫描在 10 万行时耗时可达数百毫秒,阻塞 UI 线程直接导致 ANR。 对比: 加索引后相同查询降至个位数毫秒;但不必要的索引会拖慢写入,按需建立。

5.4 KV Store 的变更监听

做法: 注册 kvStore.on('dataChange', distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_REMOTE, callback) 监听跨设备数据变更。 原因: 不注册监听时跨设备同步数据只在本地 RocksDB 中更新,UI 层无感知,用户看到"没同步"假象。 对比: 注册后跨设备延迟通常在 200ms 以内,用户几乎无感。


六、常见坑点

坑 1:Preferences 多进程写入数据丢失

  • 现象: 后台 Service 和前台 UI 同时写 Preferences,偶发数据覆盖或丢失。

  • 原因: Preferences 在每个进程中独立加载 XML 到内存,进程 A flush 后,进程 B 的内存镜像无感知,B flush 时覆盖 A 的数据。

  • 复现: 在 ServiceExtensionAbility 和 UIAbility 中同时写同一个 Preferences 文件。

  • 解决: 多进程场景改用 KV Store,或通过 IPC 集中到单进程写入。

坑 2:RelationalStore 主线程查询导致 ANR

  • 现象: 列表滚动时偶发页面冻结 2~5 秒。

  • 原因: rdbStore.query() 是耗时 I/O,在主线程调用时阻塞 UI 渲染管道。

  • 复现:aboutToAppear 中直接 await rdbStore.query(...),数据量 > 1 万行时稳定复现。

  • 解决: 所有 DB 操作移至 TaskPool 或 Worker 线程,通过 @State 回调更新 UI。

    import { taskpool } from '@kit.ArkTS';

    @Concurrent

    async function queryTask(keyword: string): Promise {

    return await queryNotes(keyword).then(r => r.map(n => n.title));

    }

    taskpool.execute(queryTask, this.keyword).then((titles: string[]) => {

    this.list = titles;

    });

坑 3:KV Store autoSync 导致流量异常

  • 现象: 应用后台流量消耗异常,单日 > 50MB。

  • 原因: autoSync: true 时每次 put() 都触发同步,高频写入导致同步风暴。

  • 复现: 循环中每 100ms 执行一次 kvStore.put(),双设备环境下流量监控立即飙升。

  • 解决: 高频写场景设 autoSync: false,积攒一批后手动调用 kvStore.sync()


七、总结

  1. 配置项 → Preferences,全量内存操作快,但不适合多进程和大数据。

  2. 跨设备共享 → KV Store,autoSync 方便但要控制频率防止流量爆炸。

  3. 结构化业务数据 → RelationalStore,必须用索引、事务、子线程三件套。

  4. DB 操作永远不要在主线程,TaskPool 是最简单的解法。

  5. 游标用完必须 close(),这是 SQLite 连接泄漏最常见的来源。

> 核心结论:先问数据量和查询复杂度,再选存储方案,而不是先选方案再填需求。


参考资料

相关推荐
小雨青年1 小时前
【鸿蒙原生开发会议随记 Pro】用 NavPathStack 收拢会议页面跳转和返回刷新
华为·harmonyos
轻口味2 小时前
轻规划鸿蒙开发实战3:AR Engine Kit 深度实践,基于面部追踪与骨骼捕捉的体感微笑打
华为·ar·harmonyos·鸿蒙
Swift社区2 小时前
鸿蒙 App 为什么需要统一状态源?
华为·harmonyos
星释2 小时前
HDC 2026 跨平台框架专题:HarmonyOS 生态下的跨端技术全景
华为·harmonyos
加农炮手Jinx3 小时前
Flutter for OpenHarmony:pub_updater 命令行工具自动更新专家(DevOps 运维必备) 深度解析与鸿蒙适配指南
android·运维·网络·flutter·华为·harmonyos·devops
yuegu7774 小时前
HarmonyOS应用<节气通>开发第21篇:CategoryGrid组件封装
华为·harmonyos
yuegu7774 小时前
HarmonyOS应用<节气通>开发第25篇:HTTP请求封装
网络协议·http·harmonyos
yuegu7774 小时前
HarmonyOS应用<节气通>开发第22篇:HolidayCard组件封装
华为·harmonyos
芒鸽4 小时前
HarmonyOS ArkUI 组件开发实战:自定义组件与高级布局详解
华为·harmonyos