ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

鸿蒙上Flutter数据库适配:用FFI自建SQLite抽象层

鸿蒙上Flutter数据库适配:用FFI自建SQLite抽象层 这篇文的起因是上个月要交付一个鸿蒙端的项目业务方指名要保留 Flutter 技术栈结果我打开 pub.dev 一翻database 相关的插件列表里全是 Android 和 iOS 标签。说实话鸿蒙那边跑 Flutter 本身已经不是最难的事了难的是 Flutter 生态里那些跟系统能力强绑定的东西——数据库就是最硬的一块。折腾了三个礼拜试过平台通道、试过替换 so 库最后我干脆把数据库访问层整个抽象了一遍让 Flutter 的通用数据库抽象能在鸿蒙上落到 SQLite 的高性能执行路径上。这篇就把整个鸿蒙化适配的思路和实战过程写下来给正被同样问题卡住的人一个参考。如果你是刚接触 Flutter 没多久的新人也能跟着走下来因为每个步骤我都尽量说清楚为什么这么做而不是只给结论。1. 鸿蒙上的 Flutter 数据库插件现状逼我不得不搞抽象1.1 一个尴尬的现状pub.dev 上全是 Android 和 iOS 标签先说我踩到的最直接的问题。项目里最初用的是 sqflite这套插件在 Android 和 iOS 上很成熟接口简单跟 SQLite 的语义几乎一一对应。但是在鸿蒙上编译通过之后运行时一执行查询报错信息就是 MissingPluginException。原因其实很容易理解sqflite 的底层是一套 Android/iOS 原生实现通过 Flutter 的 MethodChannel 暴露给 Dart 侧。鸿蒙的 Flutter 分支虽然也有 channel 机制但插件的注册路径、原生侧签名、资源打包方式跟 Android 完全不是一回事原来那套 PluginRegistry 根本不会去加载这个插件。我一开始也没多想觉得换个数据库插件总行了吧。于是试了 hive——纯 Dart 实现理论上完全跨平台但它是 NoSQL 模型存 Map 和二进制可以表关联、复杂聚合、事务回滚这些需求全得自己造轮子。又试了 isar——原生代码写得很漂亮性能也好但 isar 的文件格式和查询方言跟 SQLite 不通用团队里已经有大量 SQL 资产不可能为了鸿蒙把全项目重写一遍。换插件这条路本质上是把问题平移了要么没有 SQL 能力要么没法保证跟现有代码共用一套数据层。1.2 鸿蒙 Flutter 插件的承载形态先搞清楚敌情做适配之前我花了两天时间把鸿蒙端 Flutter 插件的运行机制摸了一遍。鸿蒙应用最终运行在 OHOS 容器里Flutter 引擎跑在 ArkTS 壳层之上。第三方插件如果要被 Flutter 调用需要以 HARHarmony Archive或本地模块的形式打包并在鸿蒙侧实现对应平台的 channel 回调。在这个架构里一个数据库插件要能跑起来至少需要三样东西Dart 侧 API、鸿蒙侧原生实现、两者之间的桥接代码。缺一样都会在运行时静默失败或者直接给你一个 MissingPluginException。这也是为什么很多 Flutter 老插件在鸿蒙上看起来编译通过、跑起来就崩。编译通过只是 Dart 层解析成功原生侧那部分根本没进去。理解了这一点你再看市面上的适配方案思路就清晰了要么给老插件补鸿蒙原生实现要么绕开平台插件机制让 Dart 直接跟后端引擎对话。1.3 我为什么从更换数据库插件转向自建抽象层经过这一轮折腾我意识到一个关键点如果继续绑定在某个插件上鸿蒙生态每前进一步我都要跟着改一轮。sqflite 适配了drift 没适配drift 适配了下个插件又冒出别的坑。与其天天追着插件版本跑不如把数据库访问收敛到自己的抽象层上底层驱动按平台自由切换Android 继续用 SQLite 原生通道iOS 继续用 FMDB鸿蒙这边自己用 FFI 直连 SQLite 动态库。接口永远是那一套只有驱动实现不同。这就是标题里说的通用数据库抽象的由来。它不是花架子而是被现实逼出来的架构决定。后面我再用三个章节讲清楚这个抽象层怎么设计、底层怎么适配、性能怎么压榨。2. 抽象层设计把 SQLite、Hive、底层驱动统统收编进来2.1 抽象层不是 ORM是方言翻译层先澄清一个容易混淆的概念。很多人听到数据库抽象层第一反应是 ORM比如 drift、TypeORM 这种把表映射成对象的框架。但我这里说的抽象层比 ORM 薄得多。它不定义对象映射只定义执行 SQL 的统一入口和管理连接、事务的统一契约。为什么这么设计因为不同平台的 SQLite 驱动API 细节差异很大有的驱动只支持同步查询有的只支持异步有的是基于回调有的是基于 Stream甚至绑定参数的占位符风格都不一样。抽象层要做的事情是把这些差异全部消化掉对外暴露一套稳定、简单、可预测的接口。好处也很直接业务代码不用关心当前跑在 Android 还是鸿蒙因为业务代码只跟这套抽象接口对话。底层驱动就算整体换掉业务层的 CRUD、事务、迁移代码一行都不用改。这在大团队里尤其重要——Dart 层和原生层可以由不同的人独立推进互相不阻塞。2.2 接口设计清单让接口少得刚好够用我最终收敛出来的核心接口不多就五个方法abstract class DatabaseAdapter { Futurevoid open(String path, {DatabaseConfig config}); FutureQueryResult query(String sql, ListObject? bindings); Futureint execute(String sql, ListObject? bindings); Futurevoid transaction(Futurevoid Function(TxExecutor executor) action); Futureint getVersion(); Futurevoid setVersion(int version); }query负责查返回行集execute负责写返回影响行数transaction包装事务块getVersion/setVersion给迁移机制用。这个接口设计的原则是少得刚好够用。我不想搞一个二十个方法的巨型抽象接口因为每多一个方法底层每个驱动就要多实现一份鸿蒙端适配的工作量就多一分。query和execute的 bindings 统一用ListObject?传到驱动层再转换成各自需要的类型。这里有个细节参数类型必须收敛int/double/String/Uint8List/bool五类够了DateTime这类复杂对象在业务层转成 ISO 字符串或毫秒时间戳再进 SQL千万别让抽象层去猜类型。2.3 驱动适配器的注册与选择平台工厂模式抽象层定义好了接下来是驱动。我用一个简单工厂来管理class Database { static DatabaseAdapterAdapter? _adapter; static void registerAdapter(DatabaseAdapter adapter) { _adapter adapter; } static DatabaseAdapter get adapter { if (_adapter null) { throw StateError(Database adapter not registered); } return _adapter!; } }应用启动时按平台注册if (Platform.isAndroid) { Database.registerAdapter(AndroidSqliteAdapter()); } else if (Platform.isHarmonyOS) { Database.registerAdapter(HarmonySqliteAdapter()); } else { Database.registerAdapter(IosSqliteAdapter()); }这个模式没什么技术含量但非常实用。测试的时候也方便你可以注册一个内存假驱动用一套 fake 数据跑业务测试完全不依赖真数据库。鸿蒙端的 HarmonySqliteAdapter 就是后面要用 FFI 实现的那个驱动它承载了所有鸿蒙级精密持久化的核心逻辑。2.4 迁移机制schema version 与增量升级数据库抽象层还有一个容易被低估的部分Schema 迁移。应用升级时老用户本地数据库可能是旧结构新代码可能是新结构两边不匹配就是一个灾难。我给抽象层定的契约是open时自动检查user_version如果低于目标版本就按版本号从小到大执行迁移脚本整个过程包在事务里任何一个迁移脚本失败就整体回滚数据库版本保持不变。Futurevoid migrate(DatabaseAdapter adapter) async { final current await adapter.getVersion(); for (var v current 1; v targetVersion; v) { await adapter.transaction((tx) async { await tx.execute(migrationScripts[v]!, []); await tx.version(v); }); } }注意setVersion要在和迁移脚本同一个事务里执行不能先执行脚本再单独改版本否则可能造成脚本执行了一半、版本号已经前进的脏状态。这个坑我早期在 Android 上踩过后来抽象层设计第一时间就把这个约束定死了。3. 三个底层适配路线我最终选了 FFI3.1 路线A平台通道MethodChannel / 鸿蒙 Napi这最传统的方案是给 sqflite 这类插件补一个鸿蒙原生实现走 MethodChannel。鸿蒙侧用 ArkTS 或 Napi 写一个数据库模块Dart 侧通过二进制消息调用。做倒是能做但我实测之后放弃了核心原因还是性能。每次 query数据要经历一遍Dart 对象 - 序列化 - 二进制消息 - 原生解析 - SQLite 执行 - 结果序列化 - 回传 Dart的完整回路。简单查询无所谓但批量写入或者复杂查询的时候这个回路的开销会非常刺眼。我做过一个对比1 万条以上批量写入channel 路线的耗能大约是纯 FFI 路线的 6 到 8 倍。而且 channel 路线的鸿蒙插件注册路径跟 Android 差异很大光是把 so 包进 HAR、处理插件签名、处理 channel 命名冲突就够喝一壶。3.2 路线BFFI 直接调用 sqlite3 动态库第二种方案也是最终方案Dart 通过dart:ffi直接加载鸿蒙 ARM64 架构下的libsqlite3.so调用sqlite3_prepare_v2、sqlite3_bind_text、sqlite3_step这些 C 接口执行 SQL。这条路线的优势是绕过了所有 channel 桥梁Dart 和 SQLite 之间只隔一层 FFI 封装。数据按二进制内存块传递不用序列化成 JSON性能损耗被压缩到最低。同时FFI 是 Dart 语言标准能力跟 Flutter 引擎版本解耦——只要引擎支持 FFI这套代码在 Android、iOS、鸿蒙上可以复用 90% 以上区别只是加载的 so 文件不同。3.3 路线Cdrift 全家桶替换 so 库drift 其实已经很接近我的做法——它底层也是 FFI 调 SQLite只要把sqlite3_flutter_libs替换成鸿蒙可用的 so 库理论上也能跑。我也确实快速验证过这条路。但 drift 的问题在于它把 ORM 层绑得太紧表对象、companion、表达式 DSL模板代码一大堆。如果团队已经习惯了手写 SQLdrift 反而是负担。再者 drift 的迁移系统是自己一套我要嵌进自己的抽象层需要写不少桥接代码。权衡下来自己维护一个轻量抽象层 FFI 驱动反而更可控。3.4 三条路线的对比决定性因素还是那两点对比维度路线AMethodChannel路线BFFI 手动封装路线Cdrift so替换性能损耗高频繁序列化低内存直通低但带ORM损耗鸿蒙适配工作量需要写原生桥接层只需编译一个so替换so但要绑ORM跨端一致性三种平台三种实现一份Dart代码三份so依赖drift维护节奏长期可维护性中原生侧代码多高抽象层稳定中升级成本大SQL自由度高高中推荐场景老项目快速迁移长期跨端产品已有drift技术栈的团队表格里最后的结论很明确只要你的项目还要在 Android/iOS/鸿蒙多条线上长期走FFI 是基本面最好的方案。鸿蒙级精密持久化这种东西本质上是三个字可控、可测、可调优。FFI 这条路线让我能直接看到 SQLite 每一层日志、每一个句柄、每一次锁等待这是 channel 黑盒路线给不了的。3.5 为什么 FFI 更符合鸿蒙级精密持久化的要求顺便说说精密这个词。鸿蒙端的 Flutter 应用有个特点主 isolate 和原生侧的内存管理模型跟 Android 不完全一样数据库连接要是被 GC 随手回收了或者被并发乱读整个页面就会卡死。FFI 让我可以在 Dart 侧精细控制连接句柄的生命周期用Finalizer绑定内存释放时机channel 路线下生命周期管理依赖原生侧消息回调悬空指针和泄漏问题更难排查。FFI 还让我可以完全绕开 channel 的异步 delay在需要同步查询的场景下直接拿到结果这对事务编排来说是一个巨大的便利。4. sqlite3 编译成鸿蒙 arm64 so 的完整过程4.1 准备三样东西再开工如果你决定跟我走同一条路第一步是把原材料备齐。首先是 sqlite3 的 amalgamation 源码从官方下载页面拿到sqlite3.c、sqlite3.h、sqlite3ext.h三件套。这个打包版本是完整核心代码合在一个文件里交叉编译最省事。其次是鸿蒙侧的 Native SDK下载后里面会包含native/build/cmake/ohos.toolchain.cmake这个工具链文件。最后是一个能跑 CMake 的构建机Windows/Linux/macOS 都行鸿蒙 SDK 的交叉编译器本身是随 SDK 分发的。有个细节必须强调一定要用和你目标设备 ABI 匹配的 SDK。现在主流鸿蒙设备都是 arm64-v8a所以下面的命令我也以arm64-v8a为例。如果你还要支持模拟器或 x86 场景再单独编一版。4.2 CMake 交叉编译的命令和关键参数我最终用的构建脚本大致长这样export OHOS_SDK_ROOT/path/to/ohos-sdk cmake -S . -B build \ -DCMAKE_TOOLCHAIN_FILE$OHOS_SDK_ROOT/native/build/cmake/ohos.toolchain.cmake \ -DOHOS_ARCHarm64-v8a \ -DOHOS_PLATFORMohos \ -DBUILD_SHARED_LIBSON \ -DCMAKE_BUILD_TYPERelease cmake --build build --target sqlite3重点是OHOS_ARCHarm64-v8a这个参数编译器、链接器、sysroot 全由工具链文件自动切换到鸿蒙目标。OHOS_PLATFORMohos表示目标是 OpenHarmony 系统。如果你缺了这一步CMake 很可能跑到你的主机平台编译器上编出来的 so 在鸿蒙设备上根本加载不了——这个坑我见不止一个人踩过。4.3 功能宏把 SQLite 的全部能力打开sqlite3 默认编译是最小可用状态很多高级能力默认关闭。为了支撑高性能 SQL 和未来可能的扩展我把常用功能宏都打开了add_library(sqlite3 SHARED sqlite3.c) target_compile_definitions(sqlite3 PRIVATE SQLITE_ENABLE_FTS5 SQLITE_ENABLE_RTREE SQLITE_ENABLE_JSON1 SQLITE_ENABLE_COLUMN_METADATA SQLITE_ENABLE_DBPAGE_VTAB SQLITE_ENABLE_DBSTAT_VTAB SQLITE_ENABLE_MATH_FUNCTIONS SQLITE_DEFAULT_SYNCHRONOUS1 )SQLITE_ENABLE_FTS5开启全文搜索SQLITE_ENABLE_JSON1支持 JSON 函数SQLITE_ENABLE_MATH_FUNCTIONS提供数学函数。后面两个 vtab 宏是在做数据库诊断时很有用的工具。SQLITE_DEFAULT_SYNCHRONOUS1是默认把同步级别设为 NORMAL配合 WAL 模式使用兼顾安全和性能。这个宏清单不一定要全部照抄但建议 FTS5 和 JSON1 至少开上因为你不知道哪天业务就需要了而重新编 so 的成本比现在多打两个宏要贵得多。4.4 Dart 侧加载 so 与符号绑定编译好之后把libsqlite3.so放进鸿蒙 HAR 的libs/arm64-v8a目录打包进应用。Dart 侧加载方式很直接final DynamicLibrary _sqlite DynamicLibrary.open(libsqlite3.so);DynamicLibrary.open会在应用沙箱的 native lib 搜索路径里找这个 so。加载成功之后用lookupFunction把 C 函数绑定成 Dart 可调用的函数类型比如typedef _OpenC Int32 Function(PointerVoid dbPtr, PointerUtf8 filename, PointerPointerPointer db); typedef _OpenDart int Function(PointerVoid dbPtr, PointerUtf8 filename, PointerPointerPointer db); final _open _sqlite.lookupFunction_OpenC, _OpenDart(sqlite3_open_v2);这里有一个新手容易踩的坑C 类型的char*在 Dart 侧要用PointerUtf8传递 Dart 字符串之前先.toNativeUtf8()用完记得free。如果字符串没转换FFI 拿到的是一堆乱码地址SQLite 执行会直接报SQLITE_MISUSE。还有一点更隐蔽so 里的符号如果被裁剪掉lookupFunction会在运行时抛ArgumentError。所以我编译时从来不开启-ffunction-sections -Wl,--gc-sections这类裁剪链接选项保证所有符号都能被找到。调试期建议先写一个最简单的sqlite3_libversion()调用验证加载成功再开始封装完整逻辑。5. 高性能 SQL 实战绑定、事务、WAL5.1 参数绑定为什么比拼串快且安全数据库适配层搭好之后真正的性能问题是下一层怎么写 SQL 更高效。我见过很多代码在 Dart 里拼 SQL 字符串比如final sql SELECT * FROM users WHERE id ${userId};这样写有两个问题。第一是安全如果userId来自用户输入SQL 注入风险直接拉满——攻击者可以构造1 OR 11之类的输入把整张表拖出来。第二是性能SQLite 每次都要重新解析、编译这条 SQL尽管结构完全相同但因为字符串内容变了解析缓存完全失效。正确的做法是准备 绑定final stmt prepare(SELECT * FROM users WHERE id ?); bindInt(stmt, 1, userId); step(stmt);SQLite 会把 SQL 语句编译成字节码缓存起来同样的语句第二次执行直接复用 plan连优化器的活都省了。实测下来循环查询一万次绑定写法比拼串写法快 30% 到 50%而且彻底杜绝了注入。这也是我抽象层里bindings参数存在的根本原因——强制业务代码走绑定路径想拼串都拼不了。5.2 批量写入实测事务把插入速度从每秒几百放到几万老生常谈但不得不谈SQLite 默认每执行一条写语句就自动 commit 一次而每次 commit 都要做磁盘 fsync。如果你循环插入一万条就要 fsync 一万次速度感人。解决办法是手动包事务await adapter.transaction((tx) async { for (final item in items) { await tx.execute( INSERT INTO events (type, payload) VALUES (?, ?), [item.type, item.payload], ); } });我实测过同一批一万条数据不开事务大概要 17 秒开了事务只花了不到 1 秒。核心原因是 commit 次数从一万次降到一次磁盘同步开销几乎被消灭。要注意的是事务块里不能出现耗时过长的外部 IO 或网络请求否则把连接占住其他业务查询只能排队。5.3 WAL 模式下的三个注意事项WALWrite-Ahead Logging是 SQLite 在高并发读多写少场景下的关键配置。开启方法就一句话PRAGMA journal_modeWAL; PRAGMA synchronousNORMAL;开启之后读操作和写操作不再互相阻塞写操作先追加到 WAL 文件再异步合并回主库文件。但有几个细节容易踩第一个是synchronousNORMAL在 WAL 模式下是安全的但在 DELETE 日志模式下风险很高。所以两个 PRAGMA 必须配套别只改一个。第二个是 WAL 会产生额外的-wal和-shm文件应用备份数据库时如果只拷贝主文件会漏掉 WAL 里的未合并数据导致备份缺数据。后面 6.3 小节我会专门说备份顺序。第三个是 WAL 文件不会无限增长SQLite 会在 checkpoint 时自动回收但如果你的应用长期以很高的写入频率运行建议后台定期PRAGMA wal_checkpoint(TRUNCATE)主动收一下避免 WAL 文件膨胀到几百 MB。5.4 EXPLAIN QUERY PLAN慢 SQL 别再靠猜很多人在 Flutter 里排查慢查询第一反应是加日志打耗时。但打日志只能告诉你这条 SQL 慢告诉不了你为什么慢。我的习惯是直接开执行计划EXPLAIN QUERY PLAN SELECT * FROM events WHERE user_id 123 AND created_at 2025-01-01;如果输出里出现SCAN events说明 SQLite 正在扫全表这是慢的根源。这时在user_id上加一个索引CREATE INDEX idx_events_user_created ON events(user_id, created_at);再跑一次执行计划输出会变成SEARCH events USING INDEX ...查询瞬间从全表扫描变成索引查找。实际项目里很多所谓的鸿蒙上数据库变慢其实不是平台问题而是索引缺失。抽象层里我也留了一个 debug 开关专门输出这类的执行计划方便线上环境快速定位慢 SQL。6. 鸿蒙数据落盘的细节沙箱、文件路径与备份6.1 别硬编码路径鸿蒙沙箱目录到底怎么取数据库适配过程中最容易出幺蛾子的其实是文件路径。Android 里你可以用getDatabasesPath()iOS 里有Documents目录但鸿蒙的应用沙箱路径规则不一样。我一开始图省事想直接在应用私有目录下写死一个路径结果数据库文件要么创建失败要么重启之后找不到。正确做法是走鸿蒙上下文接口获取沙箱目录再把路径通过一个最小的 channel 桥传给 Dart 侧。我在鸿蒙原生侧用 ArkTS 写了一个几行的取路径函数import { common } from kit.AbilityKit; function getFilesDir(): string { const context getContext(this) as common.UIAbilityContext; return context.filesDir; }拿到 base 目录后Dart 侧再拼接databases/app.db。这样做的原因是鸿蒙沙箱路径在不同版本、不同设备形态上可能都不一样硬编码就是给自己埋定时炸弹。抽象层初始化时接收的path参数也必须由上层传入而不是内部臆测——这一点在文档里要写死。6.2 数据库文件别放缓存目录被系统清掉就完了拿到filesDir之后还有一个常见误区有人图省事直接往cacheDir写数据库。缓存目录在系统存储紧张时是可以被清理的一旦被清理用户的全部本地数据就没了。正确的目录是filesDir/databases我自己还会在抽象层里做一个校验如果路径不在filesDir前缀下直接抛异常从源头避免这种看起来能跑、实际会丢数据的误用。数据库文件落盘之后目录里会长这样databases/ app.db app.db-wal app.db-shm.db-wal和.db-shm是 WAL 模式的伴生文件不是垃圾千万别手动删。文件组织好之后遇事不要慌先看这几个文件是否齐全再判断是不是并发写出问题。6.3 备份与恢复先 checkpoint 再拷贝别直接 copy备份听起来简单复制文件嘛。但在 WAL 模式下直接复制主数据库文件是个大坑WAL 文件里还有一批没合并进主库的提交你复制的快照会丢最新数据。正确顺序是执行PRAGMA wal_checkpoint(TRUNCATE)强制把 WAL 内容合回主库等 checkpoint 完成之后再用文件复制方式备份主库文件备份过程中建议只读打开数据库避免写入产生新的 WAL 内容。恢复的时候先把旧路径下的-shm和-wal文件清干净再把备份文件放回去否则 SQLite 可能基于旧 WAL 做恢复反而把数据恢复错乱。恢复完成后务必跑一次PRAGMA integrity_check确认页面没有损坏。这套流程我在桌面端和移动端都验证过稳定可靠。6.4 加密与安全存储SQLCipher 路线怎么预留鸿蒙端的数据库很多时候要存一些敏感数据比如登录 token、用户偏好、业务密钥。如果直接明文存 SQLite应用被 root 或备份提取之后数据相当于裸奔。更稳妥的路线是换成 SQLCipher一个加密版的 SQLite接口兼容打开数据库时多一个 key 参数。好消息是前面搭好的 FFI 抽象层让加密库替换成本极低——只要编译时把源码换成 SQLCipher 版本so 文件名不变接口不变唯一区别是open方法多接收一个密钥参数。我在抽象层里把DatabaseConfig设计好了encryptionKey字段Android/iOS/Harmony 三个驱动都预留了这个参数。当前如果项目还没有加密需求就把字段留空走明文未来一旦有合规要求驱动内部一行判断就能切过去。这种现在就为未来留好插槽的设计跟临时补丁完全是两个体验。7. 十次踩坑后的自查清单最后把这段时间实测过程中踩过的问题整理成一份清单。这些问题每个单独拎出来都不大但合在一起足够让人怀疑人生。希望这张单子能帮你少走几个来回。症状根因解决办法libsqlite3.so加载失败so 没被打进 HAR 的 libs 目录检查 HAR 的libs/arm64-v8a是否包含 so 文件ArgumentError: Dynamic lookup编译时符号被裁剪关闭-ffunction-sections -Wl,--gc-sectionsSQLITE_MISUSE字符串没转Utf8指针用toNativeUtf8()并在使用后释放数据库句柄被 GC 回收Dart 侧没绑定Finalizer为每个连接注册Finalizer显式控制关闭SQLITE_BUSY频繁出现多个连接并发写设置sqlite3_busy_timeout并在抽象层做连接池重启后数据库不存在路径写成硬编码用 HitMap 魔改路径重启后数据库不存在路径写成硬编码通过 context.filesDir 获取真实沙箱路径备份文件数据偏旧没 checkpoint 就复制先PRAGMA wal_checkpoint(TRUNCATE)再复制数据库损坏手动删除了-wal文件WAL 文件不能随便删恢复前先清理残留批量插入极慢没有用事务包裹一万条循环包进一个事务commit 一次查询不走索引缺少复合索引用EXPLAIN QUERY PLAN确认扫描类型某些字段存进去变乱码UTF-8 编解码没处理Dart 字符串统一走Utf8编解码再绑定有一个坑我特别想多说一句就是在鸿蒙上做热重载测试时Flutter 的 hot restart 不会自动回收上一次创建的数据库连接。如果你的open方法做的是如果文件已打开就返回旧句柄那还好如果每次都新建句柄旧的没关很快就把文件描述符耗尽表现就是莫名其妙的SQLITE_CANTOPEN。我的做法是在open入口处加一个连接表的引用计数open 前先 close 已存在的同路径连接。这种资源生命周期管理问题在传统移动平台上因为系统托管相对宽松不容易暴露在鸿蒙的 Flutter 容器里放大得非常明显。另外一个小技巧也算实践总结我在抽象层里留了一个全局 SQL 日志开关把每个事务的 SQL、绑定的参数个数、执行耗时全部打出来。上线之后一旦有人报鸿蒙端数据不对我第一件事就是打开日志开关看它实际执行了哪些语句、耗时分布在哪。一次线上偶发数据错乱最后就是靠这份日志锁定了一个驱动里int与Int64未转换的边界问题。如果你也要做鸿蒙化适配强烈建议先把这个日志基础铺好排查问题会顺畅很多。
返回列表