欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

[Flutter 持久性库漂移 - 高级功能 - 隔离

最编程 2024-03-26 18:05:50
...

「这是我参与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 的连接模型,支持不限数量的客户端。连接通过多个 SendPortReceivePort 建立。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 。可以用指定的名称存储一个 DriftIsolateconnectPort,用于后面查找。其它客户端可以使用 DriftIsolate.fromConnectPort 从名称服务器(如果有已注册的名称)中获取一个 DriftIsolate

请注意:现在的时点,对于从后台引擎中生成 isolate ,Flutter 仍然存在一些固有的问题,这使安装(准备)变得复杂。进一步来说,IsolateNameServer 并不清楚一个(无状态的)热加载,即使这些 isolate 已经停止,注册的端口也不再可用。现在没有一个可靠的方式来检查一个 SendPort 是否绑定到了一个活动的 ReceivePort 上。

这种模式可行的实现和关联的问题在此 issue 中有相关描述。