[Flutter 持久性库漂移 - 高级功能 - 隔离
「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」。
本文翻译自 drift 的 官方文档 Isolates (simonbinder.eu)。
肉翻多有不足,不吝赐教。
isolate 是 Dart 的一种隔离机制,建议搜索了解相关概念后再看本文内容。
Isolate class - dart:isolate library - Dart API
isolate
在后台 isolate 中使用 drift 数据库
安装(准备)
要使用 isolate api,首先要在工程的根目录下创建一个名为 build.yaml
的文件(靠着 pubspec.yaml
)使相应的构建选项可用。它应该有如下内容:
targets:
$default:
builders:
drift_dev:
options:
generate_connect_constructor: true
接下来,重新运行构建。现在可以向生成的数据库类中添加另外一个构造方法。
@DriftDatabase(...)
class TodoDb extends _$TodoDb {
TodoDb() : super(NativeDatabase.memory());
// 这是新的构造方法
TodoDb.connect(DatabaseConnection connection) : super.connect(connection);
}
在后台 isolate 中使用 drift
数据库类准备好以后,就可以在后台 isolate 中打开它。
import 'package:drift/isolate.dart';
// 这需要是一个*方法,因为它运行在后台 isolate 中。
DatabaseConnection _backgroundConnection() {
// 构造数据库来使用。
// 本例每次都创建了一个在内存中非持久化的数据库。
// 也可以使用文件形式的现有数据库,或者 需要异步创建的 `LazyDatabase` 。
// 当使用 `path_provider` 类似的 Flutter 插件来指定路径时,也要看一下下面 main 线程中的初始化处理。
final database = NativeDatabase.memory();
return DatabaseConnection.fromExecutor(database);
}
void main() async {
// 在一个新的后台 isolate 中创建一个 drift 执行器。
// 如果想要自己来启动 isolate ,也可以从后台 isolate 调用 DriftIsolate.inCurrent()
DriftIsolate isolate = await DriftIsolate.spawn(_backgroundConnection);
// 现在创建一个数据库连接,在内部使用 isolate 。
// 这个**不是**从 _backgroundConnection 返回的内容, drift 为 isolate 通信使用了一个内部代理类。
DatabaseConnection connection = await isolate.connect();
final db = TodoDb.connect(connection);
// 现在可以完全和往常一样使用数据库了,显然它在内部使用了后台 isolate 。
}
如果需要在 async
(异步)上下文之外构造数据库,可以使用 DatabaseConnection.delayed
构造方法。在上面示例中,可以如下同步获取一个 TodoDb
:
Future<DatabaseConnection> _connectAsync() async {
DriftIsolate isolate = await DriftIsolate.spawn(_backgroundConnection);
return isolate.connect();
}
void main() {
final db = TodoDb.connect(DatabaseConnection.delayed(_connectAsync()));
}
这对于在 DI (依赖注入)框架中使用 drift 很有帮助,因为可以立即拥有一个可用的数据库。在内部, drift 会在向数据库发送第一次查询时进行连接。
在 main 线程中初始化
平台通道(platform channel)在后台 isolate 中是不可用的,但是有时可能想要使用函数。如:使用 path_provider
中的 getApplicationDocumentsDirectory
来构造一个数据库路径。因为此函数在内部使用了方法通道,所以需要使用一个小技巧来初始化数据库。我们要手动启动运行着数据库的 isolate 。这允许我们传递在 main 线程中计算的附加数据。
Future<DriftIsolate> _createDriftIsolate() async {
// 这个方法在 main isolate 中被调用。
// 因为不能在后台 isolate 中使用 getApplicationDocumentsDirectory ,
// 所以在前台 isolate 中得出数据库的路径后,把路径通知给后台 isolate 。
final dir = await getApplicationDocumentsDirectory();
final path = p.join(dir.path, 'db.sqlite');
final receivePort = ReceivePort();
await Isolate.spawn(
_startBackground,
_IsolateStartRequest(receivePort.sendPort, path),
);
// _startBackground 会向 ReceivePort 发送这个 DriftIsolate。
return await receivePort.first as DriftIsolate;
}
void _startBackground(_IsolateStartRequest request) {
// 这是后台 isolate 的入口点!
// 用接收到的路径创建数据库
final executor = NativeDatabase(File(request.targetPath));
// 这里使用 DriftIsolate.inCurrent , 因为此方法已经在后台 isolate 上运行。
// 如果使用 DriftIsolate.spawn ,第三个 isolate 会被开启,这不是所期望的。
final driftIsolate = DriftIsolate.inCurrent(
() => DatabaseConnection.fromExecutor(executor),
);
// 通知启动关联的 isolate ,这样就可以调用 .connect() 方法了。
request.sendDriftIsolate.send(driftIsolate);
}
// 用来捆绑 SendPort 和 目标路径,因为 isolate 的入口点函数只能带一个参数。
class _IsolateStartRequest {
final SendPort sendDriftIsolate;
final String targetPath;
_IsolateStartRequest(this.sendDriftIsolate, this.targetPath);
}
再说一次,可以使用 DatabaseConnection.delayed()
为数据库类获取一个连接:
DatabaseConnection _createDriftIsolateAndConnect() {
return DatabaseConnection.delayed(() async {
final isolate = await _createDriftIsolate();
return await isolate.connect();
}());
}
初始化和后台 isolate
就像名称中隐含的意思, dart isolate (隔离) 不共享内存。这意味着全局变量和值在一个 isolate 中可以访问,但在后台 isolate 中会不可见。 例如:如果想要使用
package:sqlite3
中的open.overrideFor
,需要在实际上开着数据库的 isolate 上来使用!正如这里展示的后台 isolate ,调用open.overrideFor
的正确位置是在_startBackground
函数中,在要使用DriftIsolate.inCurrent
之前。构造数据库时可能要依赖的其它全局的域(服务定位如get_it
会浮现在脑海中)也需要单独在后台 isolate 中初始化。
关闭 isolate
由于一个指定的 DriftIsolate
中会存在多个 DatabaseConnection
,简单地调用 Database.close
不会停掉 isolate。可以使用 DriftIsolate.shutdownAll()
来实现。 这会断开所有数据库连接然后关闭后台 isoalte ,释放所有资源。
常用操作模式
可以使用 DriftIsolate
在多个 isolate 之间进行控制和连接。
一个执行者 isolate ,一个前台 isolate:
这是最常用的使用模式。可以在 Flutter 或 Dart APP 的 main
线程中调用 DriftIsolate.spawn
。和上面的示例类似,然后可以用 DriftIsolate.connect
连接 main isolate 来使用 drift,和把连接传递给生成的数据库类。
一个执行者 isolate ,多个客户端 isolate:
DriftIsolate
可以发送给多个 isolate,每个都可以使用各自的 DriftIsolate.connect
连接。在有三个或者更多的线程的地方,这对于实现一个安装(准备)是有用的。
- drift 执行者 isolate
- 一个前台 isolate,可能用于 Flutter
- 另外一个后台 isoldate,可能用于网络。
之后可以从前台 isolate 读取数据或启动查询流,和上面的示例相似。后台 isolate 也可以调用 DriftIsolate.connect
和创建自有的生成的数据库类实例。向一个数据库写入数据对于另一个 isolate 是可见的,也会更新(另一个 isolate 的)查询流。
想要安全地发送 DriftIsolate
实例穿过 SendPort
,建议发送 DriftIsolate
内部使用的底层 SendPort
来代替。
// 不要这样做,在所有情况下都不会正常运转。
void shareDriftIsolate(DriftIsolate isolate, SendPort sendPort) {
sendPort.send(isolate);
}
// 取而代之,发送底层的 SendPort :
void shareDriftIsolate(DriftIsolate isolate, SendPort sendPort) {
sendPort.send(isolate.connectPort);
}
接收结束后会使用 DriftIsolate.fromConnectPort
构造方法从一个 SendPort
重建一个 DriftIsolate
。这个 DriftIsolate
会和原始版本的行为完全一致,但是只需要发送一个原始的 SendPort
,不需要复杂的 Dart 对象。
这是如何运转的?有没有什么限制?
在后台 isolate 中,drift 的所有特性都被支持,且开箱可用。包括:
- 事务
- 自动更新的查询(即使表在另一个 isolate 中被更新)
- 批处理的更新和插入
- 自定义语句或从 sql api 生成的语句
请注意,使用后台 isolate 可以降低 UI 线程的延迟,整个数据库会变慢!这里涉及 isolate 间的数据发送的成本,但也正是 drift 内部需要做的事情。如果没有因为 drift 偶然遇到丢帧,对于应用来说,使用后台 isolate 可能不是必需的。
drift 在内部使用以下模型来实现这个 api:
-
一个服务端 isolate:
单个 isolate 执行所有查询和广播表的更新。这是由
DriftIsolate.spawn
创建的 isolate。通过类 RPC 的连接模型,支持不限数量的客户端。连接通过多个SendPort
和ReceivePort
建立。DriftIsolate
类内部只包含一个SendPort
的引用,用来和后台 isolate 建立连接。这让用户可以在多个 isolate 之间共享DriftIsolate
对象和多次连接。监听各端口的实际服务端逻辑是在私有的RunningDriftServer
类中。 -
客户端 isolate: 任意数量 isolate 中的任意数量的客户端都可以连接到一个
DriftIsolate
。客户端( isolate )充当 drift 的后台,这意味着所有的查询都是在客户端 isolate 构建。原始的 sql 字符串和参数在之后发送给服务端 isolate ,这会使操作进入队列并最终执行。在低级别实现 isolate 命令允许用户不使用 isolate api 的情况下,重用所有的代码。
独立 isolate
这里所有的安装(准备)都假设有一个 main isolate (主隔离)响应生成的 DriftIsolate
,然后它(或其它 isolate)可以连接到。
在 Flutter APP 中,这个模型可能不适合你的使用情况。例如:APP 在关闭时可能使用后台任务或接收 FCM 通知。这些任务会在由原生平台代码管理的后台 FlutterEngine
中运行,所以在 isolate 之间没有一个清晰的通信方案。尽管如此,可能想要在 UI 引擎和潜在的后台引擎间共享一个存活的 drift 数据库,即使它们之间没有直接了解。
dart:ui
中的 IsolateNameServer ( isolate 名称服务器)可用来在这类作业间透明地共享一个 drift isolate 。可以用指定的名称存储一个 DriftIsolate
的 connectPort,用于后面查找。其它客户端可以使用 DriftIsolate.fromConnectPort
从名称服务器(如果有已注册的名称)中获取一个 DriftIsolate
。
请注意:现在的时点,对于从后台引擎中生成 isolate ,Flutter 仍然存在一些固有的问题,这使安装(准备)变得复杂。进一步来说,IsolateNameServer
并不清楚一个(无状态的)热加载,即使这些 isolate 已经停止,注册的端口也不再可用。现在没有一个可靠的方式来检查一个 SendPort
是否绑定到了一个活动的 ReceivePort
上。
这种模式可行的实现和关联的问题在此 issue 中有相关描述。