
开篇一次惨痛的 ANR 事故很多做前端的同学刚转鸿蒙时做本地存储特别随意。由于 HarmonyOS 提供了非常便利的Preferences轻量级键值对存储导致大家默认把所有东西都往里塞。上个版本我们要做一个聊天列表。为了省事我把拉取到的 200 多条聊天记录包含长文本序列化成一整坨长 JSON直接塞进了Preferences。本地测试时用的是旗舰机秒存秒取。结果发布后几位使用低端机型的用户反馈一点开聊天页面应用就卡死系统直接弹出“应用无响应”ANR。我查了好久内存和 CPU Profiler最后发现在读取那个长 JSON 并反序列化时由于数据量过大达到了几百 KBUI 主线程被彻底阻塞了。这次教训让我明白在鸿蒙里存储方案的选型直接决定了应用的生死。今天我们来扒一扒Preferences和RDB关系型数据库的底层逻辑和适用边界。https://placeholder-01.jpg/二、Preferences轻量之王但也极其脆弱Preferences的底层设计是为了存储极少量的配置数据。它的核心 API 很简单typescriptimport preferences from ohos.data.preferences; // 初始化 let pref await preferences.getPreferences(getContext(this), app_settings); // 存注意 put 只是写进内存 await pref.put(theme_mode, dark); await pref.flush(); // 必须显式 flush才能写进磁盘文件 // 取 let mode await pref.get(theme_mode, light) as string;性能瓶颈的根源全量加载当应用启动时Preferences会将整个数据文件一次性全部读入内存。如果你的文件有 5MB启动时主线程就要卡顿几十毫秒。主线程序列化put和get都是异步 API但在底层复杂对象如 JSON 字符串的序列化/反序列化如果很大极易造成 CPU 阻塞。最佳实践标准只存简单的数据类型string,number,boolean。单条数据不超过8KB总量不超过1MB。绝对不要拿它存长列表、聊天记录、复杂的深层嵌套对象。它适合存的用户鉴权 Token、深色模式开关、字体大小偏好、是否同意过隐私协议。三、RDB 关系型数据库结构化数据的归宿如果你的业务是消息列表、商品列表、联系人、甚至日志。别犹豫直接上 RDB。RDB 底层基于 SQLite。它的优势在于支持 SQL 语法、支持索引加速、支持事务且能承载百万级的数据量。1. 建表与初始化在建表时一定要设计好主键和索引。typescriptimport relationalStore from ohos.data.relationalStore; const STORE_CONFIG: relationalStore.StoreConfig { name: ChatData.db, securityLevel: relationalStore.SecurityLevel.S1 // 根据数据敏感度定级 }; let rdbStore: relationalStore.RdbStore; export async function initDb(context: Context) { rdbStore await relationalStore.getRdbStore(context, STORE_CONFIG); // 建表id 作为主键msg_time 加索引用于排序查询 const SQL_CREATE CREATE TABLE IF NOT EXISTS chat_msg ( id INTEGER PRIMARY KEY AUTOINCREMENT, sender_id TEXT, content TEXT, msg_time INTEGER ) ; await rdbStore.executeSql(SQL_CREATE); await rdbStore.executeSql(CREATE INDEX IF NOT EXISTS idx_time ON chat_msg (msg_time DESC)); }2. 批量插入的性能黑洞如果你有一个同步通讯录的需求要把 5000 个联系人存进去。很多人的写法是typescript// 极其缓慢的写法循环单条插入 for (let user of userList) { await rdbStore.insert(user, user); }这段代码在真机上可能需要跑几分钟。因为每次insert都会触发一次磁盘 I/O 操作。改造方案使用事务Transaction把多个操作打包成一个事务系统只需执行一次磁盘写入。typescriptasync function batchInsertUsers(rdbStore: relationalStore.RdbStore, userList: User[]) { rdbStore.beginTransaction(); try { for (let user of userList) { await rdbStore.insert(user, user); } rdbStore.commit(); // 提交事务此时才真正落盘 } catch (err) { rdbStore.rollBack(); // 一旦发生错误全部回滚保证数据一致性 console.error(批量插入失败, err); } }用了事务5000 条数据的插入时间能缩短到 1 秒以内。3. 查询与游标释放查询完数据一定要记得调用resultSet.close()。这是新手最容易踩的坑不关游标内存会持续飙升最终导致 OOM 闪退。typescriptasync function queryRecentMsgs(rdbStore: relationalStore.RdbStore) { const predicates new relationalStore.RdbPredicates(chat_msg); predicates.orderByDesc(msg_time); predicates.limitAs(50); // 只查最近 50 条 const resultSet await rdbStore.query(predicates, [id, content, msg_time]); let msgs []; try { while (resultSet.goToNextRow()) { msgs.push({ id: resultSet.getLong(resultSet.getColumnIndex(id)), content: resultSet.getString(resultSet.getColumnIndex(content)) }); } } finally { // 必须放在 finally 里哪怕中途报错也要释放游标 resultSet.close(); } return msgs; }https://placeholder-02.png/四、数据库的版本迁移Migration这一点是决定应用后续迭代顺畅与否的关键。如果你发布了 V1.0数据库有一张表。到了 V2.0你需要加一个新字段。老用户升级后他的手机里存的还是旧版数据库。如果不做迁移直接查程序瞬间崩溃。正确的升级流程在初始化 RDB 时比对版本号。typescriptconst STORE_CONFIG: relationalStore.StoreConfig { name: AppData.db, securityLevel: relationalStore.SecurityLevel.S1 }; // 假设当前版本是 2 rdbStore await relationalStore.getRdbStore(context, STORE_CONFIG); if (rdbStore.version 0) { // 全新安装直接创建最新表结构 await createLatestTables(rdbStore); rdbStore.version 2; } else if (rdbStore.version 2) { // 老版本升级执行 ALTER TABLE await rdbStore.executeSql(ALTER TABLE user ADD COLUMN age INTEGER DEFAULT 0); rdbStore.version 2; }https://placeholder-03.png/五、实战总结与思考经过那次卡顿事故我把项目里所有乱用Preferences的地方全部重构了。我的经验总结有 UI 列表展示的必须有 RDB。不要把查询和排序的算力压在 UI 层的内存里交给底层的 SQL 引擎去做。不要在 RDB 里存 BLOB 大文件。图片、视频这种二进制文件存到沙箱files 目录数据库里只存绝对路径。加密封装。如果涉及用户敏感数据RDB 在初始化时配置encrypt: true开启加密。