端上为什么需要分层存储
应用的数据不是一个模子刻出来的:开关状态、字号选择这类只有几个字节的偏好,和成千上万条结构化记录,显然不该放在一起。鸿蒙把存储能力分成了几个层次,选择哪一层,取决于数据的体量、结构与是否需要跨端同步。
一个实用的判断标准:数据量小、结构简单、只需本机读取的,用 Preferences;数据是结构化的、需要条件查询与排序的,用关系型数据库;需要在手机、平板、车机等多设备之间自动保持一致的,用分布式数据对象。把这三层的边界想清楚,后面的代码几乎是自然推导出来的。
不要一上来就用数据库存一切。多数应用一半以上的数据其实只是几个配置项,用对工具比用强工具更重要。
这篇文章按从轻到重的顺序,把三层存储都走一遍,并在最后串成一个可跨端同步的笔记列表。
Preferences:轻量键值与配置
Preferences 是基于键值的轻量存储,适合保存用户偏好与小型状态。它的特点是读写快、以全量文件形式驻留,因此不要往里塞大对象或高频写入的列表。
import { preferences } from '@kit.ArkData';
const store = preferences.getPreferencesSync(getContext(), { name: 'user_pref' });
// 写入:键值对,第二个参数是默认值
store.putSync('font_size', 16);
store.putSync('dark_mode', false);
store.flush(); // 异步落盘,频繁写入时批量后再 flush
// 读取
const fontSize = store.getSync('font_size', 16) as number;
两个细节要注意:一是 put 之后数据先在内存,真正写入磁盘要等 flush,退出应用前记得触发一次;二是键名建议集中管理成常量,避免不同页面各写各的导致拼写不一致。监听变化用订阅接口,可以在深色模式切换时即时刷新界面。
关系型数据库:结构化数据落地
当数据变成一条条带字段的记录,并且需要按条件查询、排序、分页时,就该上关系型数据库了。鸿蒙提供的关系型数据库(RelationalStore)基于 SQLite,支持 SQL 与预编译语句。
- 字段类型文本用 TEXT,整数用 INTEGER,时间戳建议存成 Long,避免时区混乱。
- 主键每张表都要有稳定主键,列表的 ForEach 键、同步去重都靠它。
- 索引高频查询与排序的字段建索引,否则数据量一大列表就卡。
- 预编译用 statement 复用 SQL,既能防注入也能提升重复查询性能。
查询时务必带分页条件,不要一次把整张表拉进内存。配合 @State 驱动列表渲染时,建议把数据库操作放在异步任务里,查询完成再整体替换数组,避免在主线程做磁盘 IO 造成卡顿。
分布式数据对象:多端自动同步
分布式数据对象(DistributedDataObject)是鸿蒙很有特色的能力:你把它当成一个普通对象读写,框架负责在同一账号下的多台设备之间自动同步,省去了自己写同步协议。
import { distributedDataObject } from '@kit.ArkData';
// 创建一个可同步对象,并订阅同账号下的其他设备
const obj = distributedDataObject.create(getContext());
obj.set('note', '第一条笔记');
obj.set('done', false);
obj.on('status', (key: string, status: string) => {
// status 变化时刷新界面:远端改了,本地自动收到
console.info('远端更新:%{public}s => %{public}s', key, status);
});
它适合配置、购物车、编辑草稿这类小体量、强一致的场景。需要清醒认识的两点:同步依赖同一华为账号与分布式网络环境,离线时先写本地、联网再合并;冲突策略通常是后写覆盖,重要业务要自己加版本字段做更精细的合并。
数据迁移与版本升级
应用版本迭代几乎必然涉及表结构变化。关系型数据库支持配置版本号与升级回调:当本地数据库版本低于新版本时,框架会触发 onUpgrade,你在这里写 ALTER TABLE 等迁移语句。
- 新增列用 ALTER TABLE ADD COLUMN,并给默认值,避免老数据读出空值。
- 删列或改类型通常要新建表、拷贝数据、再替换,迁移前后都要备份。
- 迁移逻辑必须幂等、可重复执行,否则一次失败就会把用户数据搞坏。
- 上线前用旧版本真实数据反复演练升级流程,比在模拟器里空跑可靠得多。
数据迁移是最不能"先上线再说"的环节。一旦迁移脚本有 bug,丢掉的是真实用户的本地数据,没有第二次机会。
加密与敏感数据保护
手机号、定位、账号凭据这类数据不能裸奔在文件里。关系型数据库支持加密能力,Preferences 也可配合应用级安全存储,把敏感字段加密后再落盘。
实践上有三条底线:第一,密钥不硬编码在代码里,交给系统的密钥管理能力;第二,日志里不要打印敏感字段,hilog 也要注意脱敏;第三,卸载重装或用户登出时,按合规要求清理对应数据。这条线和下一篇要讲的应用安全与权限管理正好衔接起来。
实战:一个跨端同步的笔记列表
把关系库与分布式对象组合起来:列表数据进关系库保证查询,需要同步的关键字段用分布式对象推送,界面只认一份状态。
interface Note { id: string; text: string; done: boolean; }
@Entry
@Component
struct NotePage {
@State notes: Note[] = [];
async onPageShow() {
this.notes = await queryNotesFromDb(); // 先读本地,界面立刻有内容
this.subscribeRemote(); // 再订阅远端增量,到达后合并
}
private addNote(text: string) {
const n: Note = { id: `local-${Date.now()}`, text, done: false };
this.notes = [n].concat(this.notes); // 乐观更新
insertNoteToDb(n).then(() => pushToRemote(n));
}
build() {
List({ space: 8 }) {
ForEach(this.notes, (item: Note) => {
ListItem() { Text(item.text).fontSize(16) }
}, (item: Note) => item.id)
}
}
}
这个模式叫"本地优先":无论网络好坏,用户点下去立刻有反应,数据先落库、后台再同步。它比"等云端返回再刷新"体验好得多,代价是要处理好冲突与去重——用稳定主键做唯一标识,是整套方案的地基。
常见坑与性能建议
最后整理几个高频问题。
- 在主线程查库:大数据量查询会卡 UI,放到异步线程完成后再更新状态。
- Preferences 当数据库:塞了几百条记录,读写越来越慢,该换关系库了。
- 分布式对象不订阅状态:只 set 不 on,远端更新了本地毫无反应。
- 列表不用主键:ForEach 键用索引,跨端同步后会出现错位复用。
- 迁移不备份:改表结构前不导出旧数据,一次失败无法回滚。
存储层稳定了,应用的"记忆"才可靠。下一步建议接着读应用安全与权限管理,把数据从"存得下"做到"存得安全"。