我是兰瓶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=darklastTab=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.themeuser.<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/(持久) vs cache/(可清理) vs temp/(临时)
  • 原子写:先写临时文件,再 rename 覆盖,防止中途崩溃写坏
  • 滚动日志:按大小/日期切片,避免单文件过大
  • 清理策略:缓存加 LRU 或过期清理,避免长期膨胀
  • 安全访问:外部共享用 SAF/媒体库接口,不要直接暴露私有路径

5. 选型决策树(1 分钟定方向)

  1. 只是开关/偏好/少量 KV?Preferences

  2. 需要按条件查询/排序/分页/去重/事务?RDBStore

  3. 存二进制/大对象/导入导出/缓存?文件系统

  4. 既有结构化又有大对象?RDB + 文件(表里保存文件路径/哈希)

  5. 需要跨设备同步?

    • 轻量状态: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 操作流程

  • 新建笔记:

    1. 先在 files/notes/<id>.md 写入内容(临时 → rename)
    2. RDB 插入记录,contentPath 指向文件
  • 打开笔记:

    1. RDB 查路径 → 读取文件内容
    2. prefs.lastOpenedNoteId
  • 搜索与列表:

    • 全靠 RDB 查询(标题/更新时间/标签)

11. 常见坑位清单(别问我为什么这么熟)

  • Preferences 里塞了几百 KB 的 JSON,启动 get 卡顿 → 拆分/迁移 RDB
  • RDB 没索引,搜索一慢就全慢 → 建索引 + 分页
  • 文件写到一半崩溃,重启打不开 → 原子写 + 校验
  • 大图原图乱放,存储爆仓 → 缩略图分级 + LRU 清理
  • 升级删字段,老用户崩溃 → onUpgrade 做兼容迁移
  • 主线程做 IO → 全部改异步/后台任务

12. 速查:我该选哪个?

  • 只想存“一个开关/一个值” → Preferences
  • 要按条件查询/排序/分页 → RDBStore
  • 要存图片/音频/离线包 → 文件系统
  • 三者都有RDB(结构) + 文件(大对象) + Preferences(状态) 的“黄金三件套”

(未完待续)

Logo

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

更多推荐