最近有个朋友问了我一个很实际的问题:同样一套 Flutter 代码,在 OpenHarmony 设备上跑起来之后,原来用的 shared_preferences 直接报错,本地数据不知道存哪了。这个问题我在做跨端适配时也踩过,当时把 shared_preferences_ohos、Hive、SQLite(drift)三种方案从头到尾试了一遍,才搞清楚各自的边界。如果你正准备把一个 Flutter 项目往 OpenHarmony 上搬,或者刚开始接触 OpenHarmony 本地持久化,这篇文章应该能帮你省下不少弯路。我会从方案选型的逻辑讲到每种方案的落地细节,包含依赖配置、关键代码、踩过的坑和排障思路,几块内容都不长,但都是实操过的经验。
1. 项目背景与三种方案全景概览
1.1 为什么 OpenHarmony 上的 Flutter 持久化要单独拿出来聊
先说一个很多人容易误解的点:Flutter 官方目前支持 Android、iOS、Web、桌面这些平台,OpenHarmony 并不在官方支持列表里。咱们在 OpenHarmony 上跑 Flutter,用的是社区维护的 Flutter SDK 适配版本,构建产物也走的是 OpenHarmony 应用那套打包机制。这就带来一个连锁问题:大量依赖平台通道的插件,在 OHOS 上没有默认实现。
Android 上你写SharedPreferences.getInstance(),底层走的是 Android 原生接口;iOS 上走的是 NSUserDefaults。但在 OpenHarmony 上,如果没人给这个插件做 OHOS 平台实现,调用就是空跑或者直接抛异常。所以你会看到 pub 上出现了一批以_ohos结尾的适配包,shared_preferences_ohos就是其中一个。
另外,OpenHarmony 的沙箱目录、应用文件路径和 Android 也有差异。系统底层以 C/C++ 为主,应用侧比较常见的是 ArkTS,Flutter 应用在这里更像是一个独立的原生应用进程。目录权限、so 库加载方式、数据文件放哪,都得重新确认。这就导致本地持久化不能简单照搬原来 Android 项目的经验和路径配置,需要单独验证一遍。
1.2 三种方案到底是干什么的
先把三种工具摆清楚,后面聊选型才有基础。
- shared_preferences_ohos:最轻量的键值对存储,适合存登录标记、主题偏好、引导页是否看过这些"小配置"。它本质上是把数据写到平台侧的一个简单存储文件里,接口非常简洁。
- Hive:一个纯 Dart 实现的 NoSQL 数据库,数据以 Box 形式组织,支持自定义对象。因为不依赖平台通道,在 OpenHarmony 这类非官方支持的平台上反而很省心,基本属于拿到就能跑。
- SQLite(drift):关系型数据库方案。drift 是一个基于 SQLite 的类型安全 ORM,适合做复杂查询、表关联、事务和多表迁移。它是 Flutter 生态里做关系型持久化的主流选择,但在 OHOS 上需要额外处理 sqlite3 动态库的问题。
用生活里的场景来类比:shared_preferences 像门口的小储物柜,放钥匙、放零钱;Hive 像带标签的文件夹柜,一摞一摞分类放好;SQLite(drift)则像一整个档案室,有索引、有检索规则,能处理大量且相互关联的资料。储物柜硬塞档案肯定不行,档案室用来放钥匙又小题大做——选型错位是大多数持久化问题的源头。
2. 选型逻辑:别先找库,先看数据长什么样
2.1 三种场景对应三种数据形态
我见过不少人在选型时有一个习惯:先看哪个库热门、GitHub star 多,然后决定用它。这个思路在 OpenHarmony 上特别容易踩坑,因为适配成熟度比 star 数重要得多。我更推荐反过来,先把你应用里的数据按形态分个类,再找最匹配的库。
第一类是配置型数据。特点是数量少、每个 key 固定、value 很小,比如用户登录状态、推送开关、语言设置。这类数据用 shared_preferences_ohos 再合适不过。它的读写就是简简单单的几个方法,存下来就是一条条 KV,不需要建表也不需要索引。
第二类是对象型数据。比如本地缓存的用户信息列表、历史订单、收藏夹条目。每条数据是一个结构化的对象,可能有几十个字段,总量从几百条到上万条。这种场景用 Hive 体验很好,它天然支持对象序列化,读写速度也够快。
第三类是关系型数据。比如一个记账本,账目表、分类表、账户表之间有关联,你要按月份聚合统计、按关键字模糊搜索、做分页。这种就别用 Hive 硬凑了,老老实实上 SQLite(drift),SQL 表达能力能省一大半逻辑代码。
判断方法很简单:如果数据之间要 join、要 group by、要 where 里写复杂条件,就归到第三类;如果只是"存一个东西进去,把这个东西取出来",就归到第二类;如果连"东西"都不算,只是几个开关和标记,那就是第一类。
2.2 一张表看懂优劣势
我整理了一张选型对照表,直接从几个关键维度对比。这里的"性能"指的是我在 OpenHarmony 真机上的实际体感,不同设备会有差异,但量级可以参考。
| 对比维度 | shared_preferences_ohos | Hive | SQLite(drift) |
|---|---|---|---|
| 数据模型 | 键值对 | NoSQL 对象 | 关系型表结构 |
| 查询能力 | 按 key 取值 | 遍历、按 key 取 | 完整 SQL,join/where/聚合 |
| 类型安全 | 弱,靠运行时判断 | 中,Adapter 控制 | 强,编译期生成代码 |
| 数据量级 | 适合百级以内 | 适合万级以内 | 十万级以上也稳 |
| 平台依赖 | 依赖 OHOS 插件实现 | 纯 Dart,无平台依赖 | 依赖 sqlite3 so 库 |
| 迁移能力 | 基本没有 | 需自己写迁移逻辑 | 强,内置 migration |
| 加密支持 | 弱 | 支持 AES 加密 | 有 SQLCipher 方案,但 OHOS 上适配成本高 |
| 代码量 | 极少 | 中等 | 较多,需要建表建 DAO |
这个表不是让你背下来,而是推荐你对照自己的数据形态去勾选。能选轻的就别选重的,因为每重一档,开发成本和排障成本都明显上升。
2.3 选型决策要点总结
如果你现在还没开始写,记住下面三句话就够了:
- 90% 的 App 本地需求,用 shared_preferences_ohos + Hive 组合就能覆盖,不需要一上来就上 drift。
- 如果核心业务里有明显的表关系、统计报表、复杂筛选,直接选 drift,别用 Hive 去模拟 SQL 行为,模拟到最后代码比 SQL 还难维护。
- OpenHarmony 适配是选型时必须考虑的因素。Hive 因为纯 Dart 所以兼容性最好,shared_preferences_ohos 有现成适配包,drift 则需要多处理一个 so 库的环节,这个决定你的集成成本。
后面三节就按这三种方案分别展开,把配置、代码和坑都过一遍。
3. shared_preferences_ohos 落地:轻量 KV 的正确打开方式
3.1 环境准备与依赖配置
先说环境。OpenHarmony 上的 Flutter 开发,我建议直接用社区维护的 flutter_flutter ohos 分支,配合 DevEco Studio 的 SDK。项目创建时记得带上 ohos 平台参数:
flutter create --platforms=ohos my_app如果项目不是新建的,需要给已有项目补上 ohos 平台目录,通常是在工程根目录执行flutter create --platforms=ohos .,它会帮你生成 ohos 相关的工程结构。这里有个容易卡住的地方:构建时提示找不到 OHOS SDK。你需要把 SDK 路径配置到项目里的local.properties文件中:
ohos.sdk.dir=/path/to/ohos-sdk我遇到过"flutter run 跑不起来"的情况,十次里有八次是这里没配好。还有一个隐藏点:如果本机装有多个版本的 OHOS SDK,要注意 API 版本和 Flutter 适配版本兼容,不匹配会出现编译期各种奇怪报错。配好环境后,先跑一个空工程确认能上模拟器,再往后加依赖,排查起来会轻松很多。
然后就是引依赖。在pubspec.yaml里加上:
dependencies: shared_preferences: ^2.3.0 shared_preferences_ohos: ^1.0.3这里解释一下为什么两个都要。shared_preferences提供的是标准接口,shared_preferences_ohos是它在 OHOS 平台上的实现。构建时 Flutter 工具会根据 pubspec 中声明的插件平台配置,把 OHOS 的实现自动注册进去。如果你只加了前者,代码能引用但运行时会抛"MissingPluginException"。只加后者,又会发现部分 API 走不到统一抽象。两个一起加,才是最省心的方式。
3.2 常用 API 与避坑细节
依赖配好之后,代码使用和标准 shared_preferences 几乎一样:
final prefs = await SharedPreferences.getInstance(); // 写入 await prefs.setString('login_token', token); await prefs.setBool('has_shown_guide', true); await prefs.setInt('launch_count', 3); // 读取 final token = prefs.getString('login_token'); final count = prefs.getInt('launch_count') ?? 0; // 删除 await prefs.remove('login_token');有几个细节我想单独提醒一下。
第一,支持的数据类型有限制:String、bool、int、double、List。如果你塞一个 Map 进去,哪怕能编译过,运行期也可能出问题,建议先序列化成 String 再存。第二,所有调用都是异步的,在实际逻辑里注意 await,别在同步函数里直接取返回值,否则拿到的是 Future。第三,不要在启动时频繁调用 getInstance,这个方法在内部可能有一次平台读取,频繁调会有无谓开销,建议在应用生命周期里初始化一次,然后通过全局变量或 Provider 持有实例。
还有一点,我见过有人拿它存大量业务数据,一个 key 下面塞几千条记录。这也是个坑,因为每次读取都是整个存储文件一起加载,数据大了之后首次读取能明显感觉到卡顿。shared_preferences_ohos 定位就是轻量配置,不是数据库。
3.3 异常日志排查记录
如果你运行时报错,大概率会看到类似这样的日志:
E/flutter (31173): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: MissingPluginException(No implementation found for method getAll on channel plugins.flutter.io/shared_preferences)这个日志的意思是:Dart 侧的调用没有找到 OHOS 平台的原生实现。我当时的排查路径是这样的:
- 确认 pubspec.yaml 里
shared_preferences_ohos版本和shared_preferences主版本兼容。不兼容有时会在插件注册阶段被静默跳过。 - 检查工程目录里的
oh-package.json5,确认 ohos 插件已经出现在依赖树中。 - 清空构建产物重新跑一遍,命令是
flutter clean && flutter pub get。 - 如果还是不行,检查 OHOS SDK 的 API 版本是否和插件要求匹配。
大多数情况下,前两步就能定位。就我所知,这个插件本身还是比较稳定的,只要环境版本对齐,很少出幺蛾子。
4. Hive 集成与调优:把 Box 当成本地 NoSQL 用
4.1 纯 Dart 优势与适配方式
Hive 是我在 OpenHarmony 上最喜欢推荐给团队使用的持久化方案,原因只有一句话:它纯 Dart 实现,几乎没有平台适配负担。不像 shared_preferences_ohos 要依赖平台通道,也不像 drift 要处理 so 库,Hive 的数据文件就是应用自己管理的,写入读取都在 Dart 层完成,理论上任何能跑 Flutter 的平台上它都能跑。
集成方式很简单,pubspec.yaml里加两个包:
dependencies: hive: ^2.2.3 hive_flutter: ^1.1.0如果你用的是比较新的 Flutter/Dart 版本,原版 hive 跟进不及时,可以考虑社区维护的hive_ce,API 基本一致,但我建议先在 demo 工程里确认它能正常构建再引入正式项目。社区版和原版的差异主要在网络同步、最新 SDK 兼容性这些地方,日常用差别不大。
这里插一句要命的:你去搜"hive 优化小文件"或者"hive 窗口函数",搜出来八成是 Apache Hive 那个大数据数仓,跟 Flutter 这个 Hive 包是两个东西。我一开始也被绕晕过,两个生态的语言、存储模型、适用场景完全不同,看文章之前务必确认一下写的是哪个 Hive。
4.2 Box 设计、Adapter 注册与加密
Hive 的基本单位是 Box,你可以把它理解成一张表,但不用定义列结构。用一个简单案例来演示:我要存一个本地收藏夹列表。
首先要初始化并打开 Box:
void main() async { WidgetsFlutterBinding.ensureInitialized(); await Hive.initFlutter(); Hive.registerAdapter(CollectionItemAdapter()); await Hive.openBox<CollectionItem>('favorites'); runApp(MyApp()); }如果是自定义对象,需要写一个 TypeAdapter,这个是 Hive 序列化的关键。每个 adapter 有自己的 typeId,这个 id 在整个 App 里必须唯一。比如:
class CollectionItem { final String id; final String title; final int createdAt; CollectionItem(this.id, this.title, this.createdAt); Map<String, dynamic> toJson() => { 'id': id, 'title': title, 'createdAt': createdAt, }; factory CollectionItem.fromJson(Map<String, dynamic> json) => CollectionItem(json['id'], json['title'], json['createdAt']); } class CollectionItemAdapter extends TypeAdapter<CollectionItem> { @override final int typeId = 0; @override CollectionItem read(BinaryReader reader) { final json = reader.read() as Map; return CollectionItem.fromJson(Map<String, dynamic>.from(json)); } @override void write(BinaryWriter writer, CollectionItem obj) { writer.write(obj.toJson()); } }存取操作非常顺手:
final box = Hive.box<CollectionItem>('favorites'); await box.put('item_001', CollectionItem('001', 'Flutter 实战指南', DateTime.now().millisecondsSinceEpoch)); final item = box.get('item_001'); await box.delete('item_001');需要列出全部时,box.values返回一个可迭代集合,自己可以过滤排序。如果需要给每一行标号,可以直接用box.values.length或者自己维护一个自增计数器,并不复杂。
如果需要加密,Hive 也支持 AES 加密 Box。在openBox时传入加密 key,但要注意 key 的生成和保存策略,一旦 key 丢了,整个 Box 的数据就无法读取了。建议把加密 key 用安全方式放在应用侧之外,或者用系统级的密钥存储,这个要提前设计好。
4.3 Hive 小文件问题怎么优化
Hive 用顺手之后,下一个坑就是文件碎片。Hive 每个 Box 对应一个文件,如果 Box 开得特别多,应用目录里会散落一堆.hive文件;同一个 Box 频繁增加和删除数据,数据文件也会产生碎片。
我的优化经验有三条。
第一,控制 Box 数量。别给每一个小功能单独开一个 Box,而是按业务域归并。比如用户相关数据都放到user_box,设置相关放到settings_box,这样文件数量能压到个位数。Box 已经是内存中的 Map 了,不存在"一个 Box 只能存一种数据"的限制,塞多种类型也能正常工作。
第二,批量写入代替循环写入。需要一次写入几百条数据时,用putAll批量提交,IO 次数比逐个put少一个量级。数据量很大时还可以考虑把逻辑包在Hive.box('xxx').write(() { ... })里,这个事务接口能显著降低写入开销。
第三,定期 compact。写删频繁的 Box,数据文件可能会越来越大,虽然删除的数据在逻辑上不在了,但文件里残留了很多空隙。手动触发box.compact()可以整理文件。我一般在 App 进入后台、或者用户退出登录这类"可以承受一点额外开销"的时机去执行,避免在核心交互时打断。
还有一个小点:如果你发现 Hive 打开 Box 的耗时不正常变大,多半是文件碎片太多或者 Box 里存的单条数据太大。单条数据尽量控制在一两百 KB 以内,别把一张大图 base64 直接塞进 Box,那会让每次读文件都非常痛苦。
5. SQLite(drift)在 OpenHarmony 的落地与排障
5.1 drift 的依赖关系和 sqlite3 动态库问题
drift 是 Flutter 生态里做 SQLite 方案最成熟的选择。它的代码生成器会在编译期根据表定义生成类型安全的 DAO 代码,既能写原生 SQL,也能用编译期检查,体验比手动写 sqlite3 调用好一大截。
但有一点必须提前摆正预期:drift 本身是纯 Dart + FFI,真正干活的是 SQLite 的动态库。在 Android 上这个问题被 sqlite3_flutter_libs 自动化处理了,它会帮你在打包时打好 so 库。而在 OpenHarmony 上,这个插件直接可用的版本还不那么普遍,可能需要你自己处理 libsqlite3.so。
我当时的做法是两条路都试过:
一条路是找 OHOS 社区维护的sqlite3_flutter_libs分支版本,有的适配包会把 OpenHarmony 支持的 ABI 加上,直接依赖即可。另一条路是手动把 OpenHarmony 系统提供的libsqlite3.z.so打包进 hap,并在 FFI 层确认符号导出正常。这条路的坑在于:OpenHarmony 的 SDK 版本不同,soc 库的路径和 ABI 可能不一样,排查起来比较花时间。
依赖配置大致是这些:
dependencies: drift: ^2.15.0 drift_flutter: ^0.1.0 sqlite3_flutter_libs: ^0.5.0如果跑起来报Failed to load sqlite3或者sqlite3.so not found,就是 so 库没有正确加载。排查顺序:先看 hap 产物里是否包含 so 文件,再确认 ABI(arm64-v8a 等)匹配,最后检查 FFI 的搜索路径。这个排障过程最磨人,也最能体现 one 台设备的差异。
5.2 表设计、事务与流式查询
trift 的表定义是用 Dart 类写的,代码生成器会把它变成可执行的 SQL。比如一个记账本需要两张表:
class Accounts extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text().withLength(min: 1, max: 50)(); RealColumn get balance => real().withDefault(const Constant(0))(); } class Transactions extends Table { IntColumn get id => integer().autoIncrement()(); IntColumn get accountId => integer().references(Accounts, #id)(); TextColumn get category => text()(); RealColumn get amount => real()(); DateTimeColumn get createdAt => dateTime()(); }然后定义数据库类:
@DriftDatabase(tables: [Accounts, Transactions]) class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); @override int get schemaVersion => 1; }查询和写入的体验就比较接近写 SQL 了:
final allTransactions = await (select(db.transactions) ..where((t) => t.category.equals('餐饮')) ..orderBy((t) => (t.createdAt, OrderingMode.desc))) .get(); await db.transactions.insertOne( TransactionsCompanion.insert( accountId: 1, category: '餐饮', amount: 35.5, createdAt: DateTime.now(), ), );这种写法多了两三成样板代码,但没有 SQL 字符串拼接的坑,重构字段名也会在编译期暴露。对于复杂的查询条件,维护信心完全不一样。
drift 还有一个特别值钱的能力是watch()。它返回一个 Stream,表数据一有变化就会推送新结果。比如我的首页要展示账户总余额,就让它自动监听,余额变更后界面自动刷新,配合 Provider 或者 StreamBuilder 非常省心。如果你在搜"flutter provider 怎么用",这里可以直接落地:drift 的 Stream 就是 Provider 里一个标准的异步数据源,把watch()暴露给 ViewModel 即可。
事务和批量操作也没问题:
await db.transaction(() async { await db.into(db.accounts).insert( AccountsCompanion.insert(name: '现金', balance: 1000), ); await db.into(db.transactions).insert( TransactionsCompanion.insert(accountId: 1, category: '转账', amount: -100), ); });需要注意,表字段类型如果后续要修改,SQLite 原生不支持直接ALTER COLUMN改类型,需要走"重建表 + 拷贝数据"的迁移流程,drift 的 migration API 帮我们封装了这个过程,但每个迁移步骤还是要自己写清楚。记住一个原则:上线之前把 schemaVersion 规划和字段类型定好,改一次就是一次成本。
5.3 数据库调试与十万条数据的实测体验
本地数据库开发不可能不看数据。drift 在 OHOS 上生成的数据库文件在应用沙箱目录里,一般是app_database.db这样的名字。想直观地看表结构和试 SQL,可以把 db 文件导出来,用 DB Browser for SQLite 打开。这个工具跨平台,Windows、Linux、macOS 都有对应的安装方式,建议开发机上常备一个。
我之前被一个查询慢的问题折腾过。场景是往一张表里灌了十万条订单记录,然后做条件查询,结果一开始几秒钟都出不来。排查后发现两个原因:一是没有加索引,导致全表扫描;二是查询条件里的时间字段没有用范围匹配,而是每条都做了函数转换。解决方式是:给高频查询字段建立索引,比如在订单表的时间列上建索引,查询性能直接从秒级降到毫秒级。十万条数据对 SQLite 来说完全没有压力,真正拖慢的是没有索引的裸查询。
CREATE INDEX idx_transactions_created_at ON transactions (created_at);在 drift 里建索引可以写table 的 List<Set<Column>>,或者直接customStatement执行。实际体验下来,只要索引设计合理,十万条甚至百万条级别的本地数据查询都够用。
6. 三种方案实战对比与最终建议
6.1 实测性能与体验对比
把三种方案放在同一个 OpenHarmony 设备上做了一次简单压测,纯属个人实测,不同设备、不同数据模型会有浮动,但量级能说明问题。
| 测试项 | shared_preferences_ohos | Hive | SQLite(drift) |
|---|---|---|---|
| 写入 100 条小 KV | 约 200ms | 约 50ms | 约 80ms |
| 写入 1000 条对象 | 不建议这样用 | 约 300ms | 批量事务约 200ms |
| 读取 1000 条记录 | 全量读,随体积变慢 | 约 40ms | 约 60ms |
| 条件查询 10 万条数据 | 不支持 | 遍历,慢 | 索引后毫秒级 |
| 集成成本 | 极低 | 极低 | 中高,要处理 so 库 |
这个表的重点是:没有万能方案,只看你的瓶颈在哪。配置场景选 prefs,因为代码少;业务对象缓存选 Hive,因为快且省事;查询统计场景选 drift,因为 SQL 表达能力强,数据量大后性能可控。
6.2 数据迁移与备份策略
换方案最怕的不是写代码,而是老数据怎么办。我的建议是:
- shared_preferences_ohos 的数据文件在平台侧,备份时可以直接把整个应用沙箱目录备份出来,但恢复时要以文件级还原,不适合跨端导数据。
- Hive 的数据是以 Box 文件形式存在,可以精确到单个 Box 做备份。跨设备迁移时,直接拷贝
.hive文件到新设备相同路径,重新openBox就能读出来。 - drift 的数据库是整个
.db文件,备份用文件拷贝就行。升级表结构时,在代码里实现 migration 逻辑,旧数据会自动迁移到新表结构。
如果你是做云同步,建议在业务层做一层数据导出,比如生成 JSON 或者 SQL 脚本,不要让云端关心你用的是哪个库,这样将来换端、换存储方案都有退路。
6.3 我的组合建议
跑完一轮之后,我给自己定的组合就三条:
- 凡是"配置"类数据,比如开关、标记、登录 token,统一走 shared_preferences_ohos。它简单、可靠、代码量最少,不需要引其它重型依赖。
- 凡是"缓存"类数据,比如列表缓存、用户偏好对象、草稿,统一走 Hive。它天然支持对象,读写速度够快,在 OHOS 上适配成本几乎为零。
- 凡是"业务数据库"类数据,比如记账本、订单系统、需要筛选统计的数据,直接上 drift。虽然初始化麻烦一点,但后面的查询和迁移能帮你省下无数脑细胞。
这三个方案在一个项目里共存完全没有问题。Hive 和 drift 不冲突,prefs 和 Hive 也能和谐相处。真正需要注意的是,别在半路发现数据形态变了还在硬撑——早期两三天的重构就能解决的事,拖到发布之后再改就是高成本事故。
我个人在实际操作中的体会是:跨端适配这种事,不确定性往往比代码本身更多。你担心的"能不能跑",大部分时候验证一下就知道答案;真正消耗时间的是那些环境、版本、so 库、沙箱路径之类的隐性差异。所以在 OpenHarmony 上做本地持久化,我建议先拿一个最小的 demo 把三种方案的读写链路都跑通,再开始往正式项目里接。这个 demo 成本很低,但能帮你提前把最多的不确定性排除掉。最后再分享一个小技巧:所有持久化的读写操作都放到单独的 service 层封装,别在页面里直接调库。这样将来换方案、加缓存、做迁移,都只要改一个文件,而不是翻遍整个项目改调用点。