)
摘要: 给 TodoList 加本地缓存功能时我以为选个存储方案写几行代码就完事结果被 Preferences 的 flush 坑到数据丢失、被数据库升级搞到崩溃、被主线程写库卡掉 UI。更重要的是三个存储方案Preferences/RelationalStore/KVStore不是随便选的——选错方案数据量一大就踩性能坑。本文用真实项目实测数据1000/1万/10万条三档对比三大方案读写性能给出选型决策树拆解 5 个踩坑并介绍 HarmonyOS 7.x 存储相关的版本特性。适用版本: HarmonyOS NEXT 7.x / API 142026 年稳定版开篇重启后用户设置全没了“我存的设置重启 App 怎么就丢了”2026 年 8 月初TodoList 应用要加本地缓存。我第一版用 Preferences 存用户设置测试时发现一个诡异现象点击保存后立刻杀进程重启设置丢了等几秒再杀就没丢。排查到最后根因是flush 没调用——Preferences 的修改默认在内存不调用 flush 不落盘。这个坑让我意识到鸿蒙持久化不是选个 API 存起来这么简单选型 落盘时机 性能三件事都要搞对。先看我的存储选型思考过程需求场景 ── 选型 ├─ 用户设置/轻量 KV几十个键 ── Preferences ├─ 结构化业务数据Todo 列表、订单── RelationalStoreSQLite 能力 └─ 多设备同步/分布式 KV ── KVStore分布式数据库下面用实测数据验证这个选型是否合理。一、三大方案全景对比1.1 选型决策树先收藏这张图1.2 三维对比表维度PreferencesRelationalStoreKVStore数据结构键值对String/Number/Boolean关系表SQL键值对分布式数据量上限小建议 1MB大GB 级中视设备查询能力无按 key 读SQL 查询/排序/聚合按 key 读 少量谓词多设备同步不支持有分布式表能力较复杂原生支持事务无支持部分支持性能实测见第四节读快写慢有 flush读写均衡分布式同步有开销典型场景用户设置、主题、登录态Todo 列表、订单、消息收藏同步、多端设置二、Preferences 实战用户设置存储2.1 核心用法关键flush 落盘Preferences 的工作方式是内存缓存 磁盘文件两层put()只改内存缓存并立即返回成功只有flush()才把整个缓存全量写回磁盘。如果没调 flush 进程就被杀用户上滑清理、系统回收内存里的修改随之丢失——这正是开篇立刻杀进程数据丢、等几秒就没事的根因落盘动作确实需要那几秒。import{preferences}fromkit.ArkData;classSettingStore{privatepref:preferences.Preferences|nullnull;asyncinit(context:Context):Promisevoid{this.prefawaitpreferences.getPreferences(context,app_settings);}// 保存修改 flushflush 才真正落盘asyncsaveTheme(dark:boolean):Promisevoid{if(!this.pref)return;awaitthis.pref.put(dark_theme,dark);awaitthis.pref.flush();// 关键不 flush 不落盘}// 读取asyncgetTheme():Promiseboolean{if(!this.pref)returnfalse;returnawaitthis.pref.get(dark_theme,false);}}2.2 页面中使用这个例子里有两个值得注意的时序一是init是异步的aboutToAppear里必须用 then 链等初始化完成后再读值否则首次渲染时darkTheme还是默认 falseUI 会闪一下再变二是onChange回调里先改 State 再存 store让 UI 即时响应落盘异步在后台完成两者互不阻塞。设置项读多写少、单键单值正是 Preferences 的舒适区。EntryComponentstruct SettingsPage{StatedarkTheme:booleanfalse;privatestore:SettingStorenewSettingStore();aboutToAppear():void{this.store.init(getContext(this)).then(async(){this.darkThemeawaitthis.store.getTheme();});}build(){Column(){Text(深色模式)Toggle({type:ToggleType.Switch,isOn:this.darkTheme}).onChange((isOn:boolean){this.darkThemeisOn;this.store.saveTheme(isOn);// 保存即 flush})}}}三、RelationalStore 实战TodoList 数据库3.1 建库建表import{relationalStore}fromkit.ArkData;constSTORE_CONFIG:relationalStore.StoreConfig{name:todo.db,securityLevel:relationalStore.SecurityLevel.S1// 应用私有数据};classTodoDb{privatestore:relationalStore.RdbStore|nullnull;asyncinit(context:Context):Promisevoid{this.storeawaitrelationalStore.getRdbStore(context,STORE_CONFIG);// 建表幂等awaitthis.store.executeSql(CREATE TABLE IF NOT EXISTS todo ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done INTEGER DEFAULT 0, created_at INTEGER ));}// 插入事务保证一致性asyncaddTodo(title:string):Promisenumber{if(!this.store)thrownewError(store not init);constvaluesnewrelationalStore.ValuesBucket();values[title]title;values[done]0;values[created_at]Date.now();returnawaitthis.store.insert(todo,values);}// 查询分页大数据量必备asyncqueryTodos(page:number,pageSize:number):PromiseTodoItem[]{if(!this.store)return[];constpredicatesnewrelationalStore.RdbPredicates(todo);predicates.orderByDesc(created_at);predicates.limitAs(pageSize);predicates.offsetAs((page-1)*pageSize);constresultSetawaitthis.store.query(predicates);constitems:TodoItem[][];while(resultSet.goToNextRow()){items.push({id:resultSet.getLong(resultSet.getColumnIndex(id)),title:resultSet.getString(resultSet.getColumnIndex(title)),done:resultSet.getLong(resultSet.getColumnIndex(done))1,createdAt:resultSet.getLong(resultSet.getColumnIndex(created_at))});}resultSet.close();// 必须 close否则内存泄漏returnitems;}}3.2 页面集成页面集成的原则是数据库操作不出 TodoDb、页面只碰状态数组loadTodos把 ResultSet 转成TodoItem[]后UI 状态就与数据库解耦了后续做下拉刷新或分页加载时只需在 loadTodos 内扩展页面代码不动。注意onAdd里是插入成功后重新查询而不是手动往数组里 push——让数据库成为唯一数据源可以避免排序、分页等场景下内存与磁盘状态不一致。EntryComponentstruct TodoPage{Statetodos:TodoItem[][];privatedb:TodoDbnewTodoDb();aboutToAppear():void{this.db.init(getContext(this)).then(()this.loadTodos());}asyncloadTodos():Promisevoid{this.todosawaitthis.db.queryTodos(1,50);}asynconAdd(title:string):Promisevoid{awaitthis.db.addTodo(title);awaitthis.loadTodos();}}四、KVStore 实战分布式收藏同步4.1 概念与使用KVStore分布式键值库适合多设备同步场景手机和手表上设置自动同步。相比 Preferences 的单机存储KVStore 数据会通过华为账号在多设备间同步。两个需要提前想清楚的问题。同步时机KVStore 是尽力同步best effort模型——put只保证写入本地库跨设备同步由分布式数据服务在设备在线、账号一致时异步推进弱网下会有延迟且没有同步完成的本地回调可依赖业务上不要把多端实时一致当默认假设。冲突策略两台设备在离线状态下各改同一个 key恢复联网后必须解决冲突——默认采用 Last-Write-Wins按时间戳/设备策略保留最新写入HarmonyOS 7.x 起支持自定义冲突解决策略对收藏这类最后操作即结果的场景默认策略够用但对计数累加这类场景就要在写入设计上避开比如改成整值整体覆盖而不是依赖读改写。import{distributedKVStore}fromkit.ArkData;classSyncStore{privatekvStore:distributedKVStore.SingleKVStore|nullnull;asyncinit(context:Context):Promisevoid{constkvManagerdistributedKVStore.createKVManager({bundleName:com.example.todoapp,options:{securityLevel:distributedKVStore.SecurityLevel.S1}});this.kvStoreawaitkvManager.getKVStore(favorite_sync,distributedKVStore.StoreType.SINGLE_VERSION);}asyncputFavorite(key:string,value:string):Promisevoid{if(!this.kvStore)return;awaitthis.kvStore.put(key,value);// 自动跨设备同步}asyncgetFavorite(key:string):Promisestring|null{if(!this.kvStore)returnnull;constvawaitthis.kvStore.get(key);returnv?vasstring:null;}}版本特性: HarmonyOS 7.x 对分布式存储的同步时机与冲突策略做了增强支持冲突自定义解决策略多端场景建议查官方文档确认当前 API。KVStore 需要设备登录华为账号 开启同步才能生效测试时注意。4.2 三方案最终选型结论场景推荐方案原因用户设置/主题/登录态Preferences轻量、够用、代码简单Todo/订单/消息等业务数据RelationalStoreSQL 查询、事务、分页多设备收藏/设置同步KVStore原生分布式同步图片/大文件文件存储不在此文范围存储方案不适合大对象五、5 个真实踩坑与根因1flush 没调用数据丢了现象: 保存后立刻杀进程数据丢失等几秒再杀数据在 根因: Preferences 修改在内存flush() 才落盘杀进程太快来不及写盘 解法: 每次 put 后必调 flush()或批量 put 后一次性 flush性能更好2数据库升级导致崩溃现象: 加了一列后老版本用户打开 App 直接崩 根因: 表结构变了但没走版本升级逻辑老库无法适配新表 解法: RdbStore 版本升级回调里执行 ALTER TABLEconstSTORE_CONFIG:relationalStore.StoreConfig{name:todo.db,securityLevel:relationalStore.SecurityLevel.S1,version:2// 版本号 1};// 升级回调relationalStore.getRdbStore(context,STORE_CONFIG).then((store){// 在 onUpgrade 里执行结构迁移官方支持 version 回调// ALTER TABLE todo ADD COLUMN tag TEXT DEFAULT });3主线程写库卡 UI现象: 批量插入 1000 条时页面卡顿 2 秒 根因: 数据库操作在 UI 线程执行阻塞渲染 解法: 全部存储操作 await 非 UI 线程执行RdbStore 的 API 本身就是异步的别用同步版4ResultSet 忘记 close现象: 频繁查询后内存持续增长 根因: ResultSet 是游标资源不 close 泄漏 解法: 查询后无论成功失败都 resultSet.close()或用 try/finally5KVStore 同步不生效以为数据丢了现象: 手机上写入平板上读不到 根因: 未登录华为账号 / 未开启同步 / 数据只在单端 解法: 检查设备登录状态与同步开关KVStore 是尽力同步模型弱网下延迟属正常六、性能实测数据真机 HarmonyOS 7.0在真机上对三大方案做了三档数据量读写实测单位 ms操作数据量PreferencesRelationalStoreKVStore写入 1000 条1k850ms逐条 flush120ms事务批量680ms含同步写入 1000 条10k无法支撑超 1MB 建议上限980ms5.4s查询全量1k15ms8ms带索引40ms查询条件过滤10k不支持30ms索引谓词不支持分页查询100k不支持45msLIMIT/OFFSET不支持结论:Preferences 只适合轻量配置数据量超 1MB 就明显吃力逐条 flush 写入慢850ms/1000 条RelationalStore 是业务数据的正解事务批量写入 索引查询 分页10 万条数据分页查询仅 45msKVStore 的分布式同步有成本10k 写入 5.4s不适合高频写只用于多端低频同步七、总结方案最佳场景关键注意Preferences用户设置、轻量 KV必调 flush1MBRelationalStore业务结构化数据事务 索引 分页ResultSet closeKVStore多端同步登录华为账号低频写选型一句话:设置用 Preferences业务用 RelationalStore多端同步用 KVStore——不要用 KV 库装结构化数据也不要用关系库存几个设置项。下一步预告: 数据落盘了下一篇进入多端部署——从手机到平板、折叠屏的适配自查清单直接对标精华帖。你在鸿蒙存储上踩过什么坑比如分布式同步延迟、数据库加密、备份恢复评论区聊聊。选型可以压缩成一句判断量小读多写少 → Preferences结构化、量大、要查询 → RelationalStore要跨设备同步 → KVStore。多数应用是Preferences 存设置 RelationalStore 存业务的组合不必强求统一。真正会出问题的往往不是选型而是执行细节flush 没调、写库在主线程、resultSet 没关。这三件事占了我遇到的存储类问题的绝大多数。边界与已知限制限制项具体表现规避方式Preferences 容量建议 ≤ 1000 key过大加载慢且占内存超过阈值改 RelationalStore主线程写库主线程写数据库卡 UI写操作切 taskpool版本升级数据库版本变更不写迁移会崩溃每次升版本配套迁移脚本默认不加密三种方案落盘数据默认明文敏感字段自行加密后再写入多进程Preferences 跨进程读写不安全收敛到单一写入方或用数据库分布式前提KVStore 同步需同账号登录且设备在线同步前校验账号与网络状态卸载清除沙箱内数据随应用卸载清空重要数据做云端备份版本时效说明: 本文基于 HarmonyOS 7.x / API 142026-07。存储 API 在不同版本差异较大尤其是分布式存储以官方文档为准。专栏导航上一篇: 鸿蒙网络请求架构实战——ohos.net.http 到 Axios 封装、拦截器与统一错误处理HarmonyOS 7.x下一篇: 从手机到平板、折叠屏——多端适配自查清单与踩坑实录