sqflite_sqlcipher >2.2.1 打开 SQLCipher 3 为何报 out of memory?
结论
这类故障应先检查 SQLCipher 3 的兼容参数,以及设置这些参数的时机。打开数据库时,应依次建立连接、设置密钥、应用 SQLCipher 3 参数,最后再读取数据库页面。
部分新版 sqflite_sqlcipher 的 iOS 原生打开流程会在设置密钥后查询数据库,验证密码。这个查询早于 Dart 的 onConfigure,因此在回调中执行 PRAGMA cipher_compatibility = 3 可能已经来不及。可以提前设置进程默认兼容级别,或在原生层首次查询前配置当前连接。iOS 插件源码
仅凭这段日志,还无法确定错误码 7 的具体原因,也不能断定所有高于 2.2.1 的版本都有同一个缺陷。
为什么会在升级后失败?
SQLCipher 3 和 SQLCipher 4 的默认加密参数不同,包括页面大小、密钥派生迭代次数、KDF 和 HMAC 算法。即使密码相同,也要使用与创建文件时一致的参数才能读取。SQLCipher 4 发布说明
sqflite_sqlcipher 2.2.1 已经使用 SQLCipher 4.5.4,因此不能把这次故障解释为“2.2.1 使用 SQLCipher 3,升级后才换成 SQLCipher 4”。插件后续版本改变了打开过程中的密码校验行为,3.0.0 的更新日志记录了这项变化。2.2.1 更新日志、插件更新日志
BEGIN EXCLUSIVE 是打开流程中建立事务的语句。它出现在错误日志里,只能说明故障在执行到这里时暴露,无法据此判断事务本身有问题或设备内存不足。还需要查看此前的原生层错误,确认连接是否已因解密失败进入错误状态。
为什么 onConfigure 中的设置没有解决问题?
onConfigure 虽然是第一个 Dart 配置回调,但原生层在此之前已经打开连接并校验密钥。
相关 iOS 实现的顺序可以简化为:
创建 FMDatabaseQueue
设置密钥
查询 sqlite_schema,校验数据库是否可读
返回 Dart
执行 onConfigure
处理数据库版本和打开事务
先按默认参数打开,失败后再到 onConfigure 中补上兼容设置,无法保证有效。如果已知文件使用 SQLCipher 3,就应在首次打开目标文件前确定兼容配置。
两个 PRAGMA 的作用范围不同:
PRAGMA cipher_compatibility = 3:配置当前连接。PRAGMA cipher_default_compatibility = 3:设置进程默认值,影响之后建立的连接。
如果插件会在回调前读取数据库,可以使用后者提前配置。SQLCipher API 文档
如何正确打开这份数据库?
如果应用中的加密数据库都需要按 SQLCipher 3 默认参数读取,可以在启动时先打开一个内存数据库,设置默认兼容级别,再打开目标文件:
import 'package:sqflite_sqlcipher/sqflite.dart';
// 在启动阶段调用一次,并等待完成后再打开加密数据库。
Future<void> initializeSqlCipher3Compatibility() async {
final configDb = await openDatabase(
inMemoryDatabasePath,
singleInstance: false,
);
try {
await configDb.execute(
'PRAGMA cipher_default_compatibility = 3;',
);
} finally {
await configDb.close();
}
}
Future<Database> openSqlCipher3Database(
String databasePath,
String key,
) async {
if (key.isEmpty) {
throw ArgumentError('SQLCipher 3 数据库需要非空密钥');
}
final db = await openDatabase(
databasePath,
password: key,
singleInstance: false,
);
try {
// 实际读取数据库,验证密钥和兼容参数。
await db.rawQuery('SELECT count(*) FROM sqlite_master;');
return db;
} catch (_) {
await db.close();
rethrow;
}
}
调用顺序:
await initializeSqlCipher3Compatibility();
final db = await openSqlCipher3Database(
databasePath,
key,
);
示例省略了 version: 1,先验证已有数据库能否读取。version 表示应用的数据库结构版本,与 SQLCipher 加密格式版本无关。确认可以兼容打开后,再按实际结构版本接回 onUpgrade 等逻辑。openDatabase API
使用这一方法有两个前提:目标文件采用 SQLCipher 3 默认参数,配置连接与目标连接使用同一份原生 SQLCipher 库。它无法解决所有错误码 7 的问题。
同时使用 SQLCipher 3 和 4 数据库怎么办?
cipher_default_compatibility 会影响进程内后续建立的连接,因此不适合在并发打开不同格式数据库时反复切换。
这种情况下,可以修改或封装插件的原生打开流程,在设置密钥之后、首次校验查询之前,为目标连接执行:
PRAGMA cipher_compatibility = 3;
也可以先备份,再将旧文件迁移到 SQLCipher 4 格式。迁移会改写数据库,需要单独处理并验证结果;兼容打开本身不会升级文件格式。插件文档对 Android 自动迁移和 iOS 的处理行为有明确区分,不能假设两端一致。插件说明
仍然失败时检查什么?
- 排查是否复用了旧连接。关闭目标数据库并完整重启应用后再试。默认的
singleInstance: true可能返回已有实例,后一次调用传入的回调和参数会被忽略;示例使用false是为了便于验证。 - 核对实际依赖。比较升级前后的
pubspec.lock、Podfile.lock,并用PRAGMA cipher_version;查看运行时加载的 SQLCipher 版本。这个值表示库版本,不表示文件格式。 - 核对建库参数。如果旧数据库使用了自定义页面大小或 KDF 参数,仅设置兼容级别 3 仍可能失败。
- 保留首次异常。原代码在
catch内重试,重试时再次抛出的异常会继续向上传播。调用方仍需处理打开失败,避免出现日志中的Unhandled Exception。
先确认使用当前密钥和参数能成功读取目标文件,再恢复数据库版本管理逻辑,之后才能判断剩余问题是否与插件回归或实际的内存分配失败有关。
备注:内容仅供参考。
