Android学Dart学习笔记第二十八节 Isolates

概览

只要你的应用程序在处理那些大到足以暂时阻塞其他计算的任务时,就应该使用隔离区。

最常见的例子是在 Flutter 应用中,当你需要执行大量计算时,否则这些计算可能会导致用户界面变得无响应。

关于何时必须使用隔离区,并没有明确的规定,但在以下一些情况下,它们会很有用:

・解析和解码特别大的 JSON 数据块。

・处理和压缩照片、音频和视频。

・转换音频和视频文件。

・在大型列表或文件系统中执行复杂的搜索和筛选。

・执行输入 / 输出操作,例如与数据库通信。

・处理大量的网络请求。

Implementing a simple worker isolate

这些示例演示了如何在主Isolate生成一个简单的工作Isolate。

Isolate.run() 简化了设置和管理工作Isolate背后的步骤:

  • 生成(启动并创建)一个Isolate。
  • 在生成的Isolate上运行一个函数。
  • 捕获函数执行的结果。
  • 将结果返回给主Isolate。
  • 工作完成后终止该Isolate。
  • 检查、捕获异常和错误,并将其抛回主Isolate。

Running an existing method in a new isolate

调用 run () 来生成一个新的隔离区(一个后台工作程序),直接在主隔离区中进行,同时 main () 等待结果:

Isolate.run () 接收 _readAndParseJson () 返回的结果,并将该值发送回主隔离区,同时关闭工作隔离区。

工作线程隔离区将存储结果的内存转移到主隔离区。它不会复制数据。工作线程隔离区会执行一次验证过程,以确保这些对象是允许被转移的。

_readAndParseJson () 是一个现有的异步函数,它完全可以直接在主隔离区中运行。而使用 Isolate.run () 来运行它则能够实现并发。工作隔离区完全抽象了 _readAndParseJson () 的计算过程。它可以在不阻塞主隔离区的情况下完成运行。

Isolate.run () 的结果始终是一个 Future,因为主隔离区中的代码会继续运行。

工作隔离区执行的计算是同步的还是异步的,都不会影响主隔离区,因为无论哪种情况,工作隔离区都是并发运行的。

Sending closures with isolates(使用隔离区发送闭包)

你也可以在主隔离区中使用函数字面量或闭包,通过 run () 来创建一个简单的工作隔离区。

复制代码
const String filename = 'bin/a.txt';

void main() async {
  final jsonData = await Isolate.run(() async {
    final fileData = await File(filename).readAsString();
    final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
    return jsonData;
  });
  print('Number of JSON keys: ${jsonData.length}');
}

这个示例实现了与前一个示例相同的功能:生成一个新的Isolate,执行计算,并将结果传回。

但这次的不同之处在于,Isolate传递的是一个闭包。相比常规的命名函数,闭包在运作方式和代码编写形式上都更为灵活

在本示例中,Isolate.run() 执行了一段看起来像是本地代码的代码,但实际是并发执行的。从这个意义上说,您可以将 run() 视为一种"并行运行"的控制流操作符。

Sending multiple messages between isolates with ports

通过端口在隔离区之间发送多条消息'。

短期存在的隔离区使用起来很方便,但生成新隔离区以及在不同隔离区之间复制对象会产生性能开销。如果你的代码依赖于通过 Isolate.run 重复运行相同的计算,那么创建不会立即退出的长期存在的隔离区或许能提高性能。

要做到这一点,你可以使用一些被 Isolate.run 封装的底层隔离 API:

ReceivePort and SendPort

在隔离区之间建立长期通信需要两个类(除了 Isolate 之外):ReceivePort 和 SendPort。这些端口是隔离区之间能够相互通信的唯一方式。

ReceivePort 是一个用于处理从其他隔离区发送来的消息的对象。这些消息是通过 SendPort 发送的。

一个 SendPort 对象恰好与一个 ReceivePort 相关联,但一个 ReceivePort 可以有多个 SendPort。当你创建一个 ReceivePort 时,它会为自己创建一个 SendPort。你可以创建额外的 SendPort,以便向现有的 ReceivePort 发送消息。

端口的行为与 Stream 对象类似(实际上,接收端口实现了 Stream!)。你可以把 SendPort 和 ReceivePort 分别看作是 Stream 的 StreamController 和监听器。SendPort 就像一个 StreamController,因为你可以使用 SendPort.send () 方法向它们 "添加" 消息,而这些消息会由监听器处理,在这里就是 ReceivePort。然后,ReceivePort 通过将接收到的消息作为参数传递给你提供的回调函数来处理这些消息。

设置端口

新生成的隔离区仅包含通过 Isolate.spawn 调用接收的信息。

如果需要主隔离区在新生成的隔离区创建后继续与其通信,就必须建立一个通信通道,以便新生成的隔离区能向主隔离区发送消息。

隔离区之间只能通过消息传递进行通信。它们无法 "看到" 彼此内存中的内容,这也是 "隔离区" 这一名称的由来。

要建立这种双向通信,首先在主隔离区中创建一个接收端口(ReceivePort),然后在使用 Isolate.spawn 生成新隔离区时,将其发送端口(SendPort)作为参数传递给新隔离区。接着,新隔离区创建自己的接收端口,并通过主隔离区传递给它的发送端口,将自己的发送端口发送回去。主隔离区接收到这个发送端口后,双方就都拥有了一个可以发送和接收消息的开放通道。

  1. 在主Isolate中创建一个ReceivePort,SendPort会自动作为ReceivePort的属性生成。
  2. 使用Isolate.spawn()生成工作Isolate。
  3. 将ReceivePort.sendPort的引用作为第一条消息传递给工作Isolate。
  4. 在工作Isolate中创建另一个新的ReceivePort。
  5. 将工作Isolate的ReceivePort.sendPort的引用作为第一条消息传回给主Isolate。

除了创建端口和设置通信外,你还需要告知端口在收到消息时该执行什么操作。这可以通过在各个相应的接收端口(ReceivePort)上使用监听(listen)方法来实现。

  1. 通过主Isolate持有的工作Isolate的SendPort引用发送消息。
  2. 在工作Isolate的ReceivePort监听器上接收并处理该消息,此处将执行需要移出主Isolate的计算任务。
  3. 通过工作Isolate持有的主Isolate的SendPort引用发送返回消息。
  4. 在主Isolate的ReceivePort监听器上接收返回的消息。
基本端口示例

此示例展示了如何设置一个长期存在的工作器隔离区,并实现它与主隔离区之间的双向通信。该代码以向新隔离区发送 JSON 文本为例,JSON 文本在新隔离区中会被解析和解码,然后再发送回主隔离区。

步骤 1:定义工作类

首先,为您的后台工作Isolate创建一个类。该类需要包含以下所有功能:

  • 生成一个Isolate。
  • 向该Isolate发送消息。
  • 使该Isolate能够解码部分JSON数据。
  • 将解码后的JSON数据发送回主Isolate。

这个类公开了两个公共方法:一个用于启动工作隔离区,另一个用于处理向该工作隔离区发送消息。

复制代码
class Worker {
  Future<void> spawn() async {
    // TODO: Add functionality to spawn a worker isolate.
  }

  void _handleResponsesFromIsolate(dynamic message) {
    // TODO: Handle messages sent back from the worker isolate.
  }

  static void _startRemoteIsolate(SendPort port) {
    // TODO: Define code that should be executed on the worker isolate.
  }

  Future<void> parseJson(String message) async {
    // TODO: Define a public method that can
    // be used to send messages to the worker isolate.
  }
}

步骤 2:生成一个工作隔离区

首先创建一个 ReceivePort,使主Isolate能够接收来自新生成的工作者Isolate发送的消息。

接着为接收端口添加监听器,以处理工作者Isolate将传回的响应消息。传递给监听器的回调函数 _handleResponsesFromIsolate 将在第4步详细说明。

最后,使用 Isolate.spawn 生成工作者Isolate。该方法需要两个参数:一个将在工作者Isolate上执行的函数(详见第3步),以及接收端口的 sendPort 属性。

复制代码
Future<void> spawn() async {
  final receivePort = ReceivePort();
  receivePort.listen(_handleResponsesFromIsolate);
  await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort);
}

当在工作隔离区调用回调函数(_startRemoteIsolate)时,receivePort.sendPort 参数会作为一个参数传递给该回调函数。这是确保工作隔离区能够有办法向主隔离区发送消息的第一步。

步骤 3:在工作隔离区上执行代码

在这一步中,你要定义方法_startRemoteIsolate,该方法会被发送到工作隔离区,以便在其生成时执行。这个方法就像是工作隔离区的 "主" 方法。

首先,创建另一个新的 ReceivePort,该端口用于接收来自主Isolate的后续消息。

接着,将此端口的 SendPort 发送回主Isolate。

最后,向新创建的 ReceivePort 添加一个监听器。该监听器用于处理主Isolate发送给工作Isolate的消息。

复制代码
static void _startRemoteIsolate(SendPort port) {
  final receivePort = ReceivePort();//工作线程的消息接收器
  port.send(receivePort.sendPort);//将工作线程的消息发送器 发送 给主隔离区

  receivePort.listen((dynamic message) async {//开启工作线程内消息的接收
    if (message is String) {
      final transformed = jsonDecode(message);
      port.send(transformed);
    }
  });
}

工作线程的接收端口上的监听器会对从主隔离区传来的 JSON 进行解码,然后将解码后的 JSON 发回主隔离区。

这个监听器是从主隔离区发送到工作隔离区的消息的入口点。这是你告诉工作隔离区未来要执行哪些代码的唯一机会

步骤 4:在主隔离区处理消息

最后,你需要告知主隔离区如何处理从工作隔离区发送回主隔离区的消息。要做到这一点,你需要填充_handleResponsesFromIsolate 方法。回想一下,如步骤 2 中所述,此方法会被传递给 receivePort.listen 方法:

还要记得在步骤 3 中,你向主隔离区发送了一个 SendPort。此方法会处理该 SendPort 的接收,同时也会处理后续的消息(这些消息将是解码后的 JSON)。

首先,检查该消息是否为 SendPort。如果是,就将该端口分配给类的_sendPort 属性,以便日后用于发送消息。

接下来,检查该消息是否为 Map<String, dynamic> 类型,这是解码后的 JSON 的预期类型。如果是,则用你的应用特定逻辑处理该消息。

在这个示例中,会打印该消息。

复制代码
void _handleResponsesFromIsolate(dynamic message) {
  if (message is SendPort) {
    _sendPort = message;
    _isolateReady.complete();
  } else if (message is Map<String, dynamic>) {
    print(message);
  }
}

步骤 5:添加一个完成器,以确保你的隔离区已设置好

要完成这个类,需定义一个名为 parseJson 的公共方法,该方法负责向工作隔离区发送消息。它还需要确保能在隔离区完全设置好之前发送消息。为处理这种情况,请使用 Completer。

首先,在类级别添加一个名为 _isolateReady 的 Completer 属性。

接着,在 _handleResponsesFromIsolate 方法(在步骤4中创建)中,如果接收到的消息是 SendPort 类型,则调用该 Completer 的 complete() 方法。

最后,在 parseJson 方法中,执行 _sendPort.send 之前添加 await _isolateReady.future。这确保了在工作Isolate完成生成并将其 SendPort 发送回主Isolate之前,不会向该Isolate发送任何消息。

复制代码
Future<void> parseJson(String message) async {
  await _isolateReady.future;
  _sendPort.send(message);
}

最后,附上此案例的完整代码

复制代码
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';

void main() async {
  final worker = Worker();
  await worker.spawn();
  await worker.parseJson('{"key":"value"}');
}

class Worker {
  late SendPort _sendPort;
  final Completer<void> _isolateReady = Completer.sync();

  Future<void> spawn() async {
    final receivePort = ReceivePort();
    receivePort.listen(_handleResponsesFromIsolate);
    await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort);
  }

  void _handleResponsesFromIsolate(dynamic message) {
    if (message is SendPort) {
      _sendPort = message;
      _isolateReady.complete();
    } else if (message is Map<String, dynamic>) {
      print(message);
    }
  }

  static void _startRemoteIsolate(SendPort port) {
    final receivePort = ReceivePort();
    port.send(receivePort.sendPort);

    receivePort.listen((dynamic message) async {
      if (message is String) {
        final transformed = jsonDecode(message);
        port.send(transformed);
      }
    });
  }

  Future<void> parseJson(String message) async {
    await _isolateReady.future;
    _sendPort.send(message);
  }

}

稳定端口示例

前面的示例解释了设置具有双向通信功能的长期存在的隔离区所需的基本构建块。如前所述,该示例缺少一些重要功能,例如错误处理、在端口不再使用时关闭它们的能力,以及在某些情况下消息排序不一致的问题

这个示例通过创建一个具有更多附加功能且遵循更好设计模式的长生命周期工作隔离区,对第一个示例中的信息进行了扩展。尽管此代码与第一个示例有相似之处,但它不是该示例的扩展。

第一步:定义工作者类

首先,为您的后台工作Isolate创建一个类。该类包含实现以下功能所需的所有方法:

  • 生成一个Isolate
  • 向该Isolate发送消息
  • 使Isolate能够解码JSON数据
  • 将解码后的JSON数据发送回主Isolate

该类公开三个公共方法:一个用于创建工作Isolate,一个用于处理向该工作Isolate发送消息,以及一个在端口不再使用时关闭它们的清理方法。

复制代码
class Worker {
  final SendPort _commands;
  final ReceivePort _responses;

  Future<Object?> parseJson(String message) async {
    // TODO: Ensure the port is still open.
    _commands.send(message);
  }

  static Future<Worker> spawn() async {
    // TODO: Add functionality to create a new Worker object with a
    //  connection to a spawned isolate.
    throw UnimplementedError();
  }

  Worker._(this._responses, this._commands) {
    // TODO: Initialize main isolate receive port listener.
  }

  void _handleResponsesFromIsolate(dynamic message) {
    // TODO: Handle messages sent back from the worker isolate.
  }

  static void _handleCommandsToIsolate(ReceivePort rp, SendPort sp) async {
    // TODO: Handle messages sent back from the worker isolate.
  }

  static void _startRemoteIsolate(SendPort sp) {
    // TODO: Initialize worker isolate's ports.
  }
}

在这个示例中,SendPort 和 ReceivePort 实例遵循了一种最佳实践命名约定,即它们的命名与主隔离区相关。通过 SendPort 从主隔离区发送到工作隔离区的消息被称为command ,而发送回主隔离区的消息则被称为response

步骤 2:在 Worker.spawn 方法中创建一个 RawReceivePort

在生成隔离区之前,你需要创建一个 RawReceivePort,它是一种较低级别的 ReceivePort。使用 RawReceivePort 是一种更优的模式,因为它能让你将隔离区的启动逻辑与处理隔离区上消息传递的逻辑分离开来。

在 Worker.spawn 方法中:

首先,创建一个 RawReceivePort。该端口仅负责接收来自工作Isolate的初始消息(该消息将是一个 SendPort)。

接着,创建一个 Completer,用于指示Isolate何时准备就绪可接收消息。当该Completer完成时,它将返回一个包含 ReceivePort 和 SendPort 的记录。

然后,定义 RawReceivePort.handler 属性。该属性是一个 Function? 类型,其行为类似于 ReceivePort.listener。当此端口收到消息时,该函数将被调用。

在 handler 函数内部,调用 connection.complete()。此方法需要接收一个包含 ReceivePort 和 SendPort 的记录作为参数。这里的 SendPort 即工作Isolate发送的初始消息,下一步会将其赋值给类层级的名为 _commands 的 SendPort 属性。

随后,使用 ReceivePort.fromRawReceivePort 构造函数创建一个新的 ReceivePort,并传入 initPort 作为参数。

复制代码
class Worker {
  final SendPort _commands;
  final ReceivePort _responses;

  static Future<Worker> spawn() async {
    // Create a receive port and add its initial message handler.
    final initPort = RawReceivePort();
    final connection = Completer<(ReceivePort, SendPort)>.sync();
    initPort.handler = (initialMessage) {
      final commandPort = initialMessage as SendPort;
      connection.complete((
        ReceivePort.fromRawReceivePort(initPort),
        commandPort,
      ));
    };
  }
}

先创建一个 RawReceivePort,然后再创建一个 ReceivePort,这样你之后就能向 ReceivePort.listen 添加新的回调。相反,如果你直接创建一个 ReceivePort,你将只能添加一个监听器,因为 ReceivePort 实现的是 Stream,而不是 BroadcastStream。

实际上,这使你能够将隔离区的启动逻辑与在通信设置完成后处理接收消息的逻辑分离开来。随着其他方法中逻辑的增多,这一优势将变得更加明显。

步骤 3:使用 Isolate.spawn 生成一个工作隔离区

此步骤将继续完善 Worker.spawn 方法。您将添加生成Isolate所需的代码,并返回该类的 Worker 实例。在此示例中,对 Isolate.spawn 的调用被包装在 try/catch 块中,以确保若Isolate启动失败,将关闭前一步创建的接收端口,且不会创建 Worker 对象。

首先,在 try/catch 块中尝试生成工作Isolate。如果生成失败,则关闭上一步创建的接收端口。传递给 Isolate.spawn 的方法将在后续步骤中说明。

接着,等待 connection.future 完成,并从其返回的记录中解构出发送端口和接收端口。

最后,通过调用 Worker 的私有构造函数并传入来自该Completer的端口,返回一个 Worker 实例。

复制代码
class Worker {
  final SendPort _commands;
  final ReceivePort _responses;

  static Future<Worker> spawn() async {
    // Create a receive port and add its initial message handler.
    final initPort = RawReceivePort();
    final connection = Completer<(ReceivePort, SendPort)>.sync();
    initPort.handler = (initialMessage) {
      final commandPort = initialMessage as SendPort;
      connection.complete((
        ReceivePort.fromRawReceivePort(initPort),
        commandPort,
      ));
    };
    // Spawn the isolate.
    try {
      await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort));
    } on Object {
      initPort.close();
      rethrow;
    }

    final (ReceivePort receivePort, SendPort sendPort) =
        await connection.future;

    return Worker._(receivePort, sendPort);
  }
}

请注意,在这个示例中(与前一个示例相比),Worker.spawn 充当了这个类的异步静态构造函数,并且是创建 Worker 实例的唯一方式。这简化了 API,使得创建 Worker 实例的代码更加简洁。

步骤 4:完成隔离设置过程

在这一步中,你将完成基本的隔离环境设置过程。这几乎完全与前面的示例相关,并且没有新的概念。略有不同的是,代码被拆分成了更多的方法。

首先,创建从 Worker.spawn 方法返回的私有构造函数。在构造函数体中,为主隔离区使用的接收端口添加一个监听器,并将一个尚未定义的名为_handleResponsesFromIsolate 的方法传递给该监听器。

复制代码
class Worker {
  final SendPort _commands;
  final ReceivePort _responses;

  Worker._(this._responses, this._commands) {
    _responses.listen(_handleResponsesFromIsolate);
  }
}

接下来,向_startRemoteIsolate 添加负责初始化工作隔离区上端口的代码。回想一下,这个方法是在 Worker.spawn 方法中传递给 Isolate.spawn 的,并且它会接收主隔离区的 SendPort 作为参数。

  1. 创建一个新的 ReceivePort。

  2. 将该端口的 SendPort 发送回主Isolate。

  3. 调用名为 _handleCommandsToIsolate 的新方法,并将新创建的 ReceivePort 以及主Isolate的 SendPort 作为参数传入。

    static void _startRemoteIsolate(SendPort sendPort) {
    final receivePort = ReceivePort();
    sendPort.send(receivePort.sendPort);
    _handleCommandsToIsolate(receivePort, sendPort);
    }

接下来,添加_handleCommandsToIsolate 方法,该方法负责从主隔离区接收消息,在工作隔离区解码 json,并将解码后的 json 作为响应发送回去。

  1. 首先,在工作Isolate的 ReceivePort 上声明一个监听器。

  2. 在添加至监听器的回调函数中,尝试在 try/catch 块内解码从主Isolate传递过来的JSON数据。若解码成功,则将解码后的JSON发送回主Isolate。

  3. 若出现错误,则传回一个 RemoteError。

    static void _handleCommandsToIsolate(
    ReceivePort receivePort,
    SendPort sendPort,
    ) {
    receivePort.listen((message) {
    try {
    final jsonData = jsonDecode(message as String);
    sendPort.send(jsonData);
    } catch (e) {
    sendPort.send(RemoteError(e.toString(), ''));
    }
    });
    }

接下来,添加_handleResponsesFromIsolate 方法的代码。

  1. 首先,检查消息是否为 RemoteError 类型,若是则抛出该错误。

  2. 若非错误消息,则打印该消息。在后续步骤中,您将更新此代码,使其能够返回消息而非仅打印。

    void _handleResponsesFromIsolate(dynamic message) {
    if (message is RemoteError) {
    throw message;
    } else {
    print(message);
    }
    }

最后,添加 parseJson 方法,这是一个公共方法,允许外部代码将 JSON 发送到工作隔离区进行解码。

复制代码
Future<Object?> parseJson(String message) async {
  _commands.send(message);
}

步骤 5:同时处理多条消息

目前,如果你快速向工作隔离区发送消息,该隔离区会按照消息处理完成的顺序发送解码后的 JSON 响应,而非消息发送的顺序。你无法确定哪个响应对应哪个消息。

在这一步中,你将通过为每条消息分配一个 ID,并使用 Completer 对象来解决这个问题,以确保当外部代码调用 parseJson 时,返回给该调用者的是正确的响应。

首先,向 Worker 添加两个类级别的属性:

Map<int, Completer<Object?>> _activeRequests

int _idCounter

复制代码
class Worker {
  final SendPort _commands;
  final ReceivePort _responses;
  final Map<int, Completer<Object?>> _activeRequests = {};
  int _idCounter = 0;
  // ···
}

_activeRequests 映射将发送给工作隔离区的消息与一个 Completer 相关联。_activeRequests 中使用的键来自 _idCounter,随着更多消息的发送,_idCounter 会递增。

接下来,更新 parseJson 方法,使其在向工作隔离区发送消息之前创建完成器。

  1. 首先创建一个 Completer。

  2. 接着递增 _idCounter,确保每个 Completer 都与一个唯一编号关联。

  3. 在 _activeRequests 映射中添加一个条目:键为当前 _idCounter 的值,值为刚创建的 Completer。

  4. 将消息连同该ID一起发送给工作Isolate。由于通过 SendPort 只能发送一个值,需要将ID和消息包装在一个记录(record)中发送。

  5. 最后,返回该 Completer 的 future 对象,该 future 最终将包含来自工作Isolate的响应。

    Future<Object?> parseJson(String message) async {
    final completer = Completer<Object?>.sync();
    final id = _idCounter++;
    _activeRequests[id] = completer;
    _commands.send((id, message));
    return await completer.future;
    }

你还需要更新_handleResponsesFromIsolate 和_handleCommandsToIsolate 来处理这个系统。

在_handleCommandsToIsolate 中,你需要考虑到消息是一个包含两个值的记录,而不仅仅是 JSON 文本。可以通过从消息中解构这些值来实现。

然后,在解码 JSON 之后,更新对 sendPort.send 的调用,使用一个记录将 id 和解码后的 JSON 都传递回主隔离区。

复制代码
static void _handleCommandsToIsolate(
  ReceivePort receivePort,
  SendPort sendPort,
) {
  receivePort.listen((message) {
    final (int id, String jsonText) = message as (int, String); // New
    try {
      final jsonData = jsonDecode(jsonText);
      sendPort.send((id, jsonData)); // Updated
    } catch (e) {
      sendPort.send((id, RemoteError(e.toString(), '')));
    }
  });
}

最后,更新_handleResponsesFromIsolate。

首先,从消息参数中再次解构出ID和响应数据。

接着,从 _activeRequests 映射中移除与该请求对应的completer。

最后,不再抛出错误或打印解码后的JSON,而是通过传入响应数据来完成completer。当此操作完成后,响应将返回给在主Isolate上调用 parseJson 的代码。

复制代码
void _handleResponsesFromIsolate(dynamic message) {
  final (int id, Object? response) = message as (int, Object?); // New
  final completer = _activeRequests.remove(id)!; // New

  if (response is RemoteError) {
    completer.completeError(response); // Updated
  } else {
    completer.complete(response); // Updated
  }
}

步骤 6:添加关闭端口的功能

当你的代码不再使用隔离区时,你应该关闭主隔离区和工作隔离区上的端口。

  1. 首先,在类级别添加一个布尔变量,用于跟踪端口是否已关闭。

  2. 接着,添加 Worker.close 方法。在该方法内:

    将 _closed 更新为 true。

    向工作Isolate发送一条最终消息(此处为字符串"shutdown",但您可以使用任何对象)。这条消息将在下一段代码片段中使用。

  3. 最后,检查 _activeRequests 是否为空。如果为空,则关闭主Isolate中名为 _responses 的 ReceivePort。

    class Worker {
    bool _closed = false;
    // ···
    void close() {
    if (!_closed) {
    _closed = true;
    _commands.send('shutdown');
    if (_activeRequests.isEmpty) _responses.close();
    print('--- port closed --- ');
    }
    }
    }

接下来,你需要在工作隔离区中处理 "shutdown" 消息。将以下代码添加到_handleCommandsToIsolate 方法中。这段代码会检查该消息是否为一个内容为 "shutdown" 的字符串。如果是,它将关闭工作隔离区的 ReceivePort,然后返回。

复制代码
static void _handleCommandsToIsolate(
  ReceivePort receivePort,
  SendPort sendPort,
) {
  receivePort.listen((message) {
    // New if-block.
    if (message == 'shutdown') {
      receivePort.close();
      return;
    }
    final (int id, String jsonText) = message as (int, String);
    try {
      final jsonData = jsonDecode(jsonText);
      sendPort.send((id, jsonData));
    } catch (e) {
      sendPort.send((id, RemoteError(e.toString(), '')));
    }
  });
}

最后,你应该添加代码,在尝试发送消息之前检查端口是否已关闭。在 Worker.parseJson 方法中添加一行代码。

复制代码
Future<Object?> parseJson(String message) async {
  if (_closed) throw StateError('Closed'); // New
  final completer = Completer<Object?>.sync();
  final id = _idCounter++;
  _activeRequests[id] = completer;
  _commands.send((id, message));
  return await completer.future;
}

最后附上完整代码

复制代码
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';

void main() async {
  final worker = await Worker.spawn();
  print(await worker.parseJson('{"key":"value"}'));
  print(await worker.parseJson('"banana"'));
  print(await worker.parseJson('[true, false, null, 1, "string"]'));
  print(
    await Future.wait([worker.parseJson('"yes"'), worker.parseJson('"no"')]),
  );
  worker.close();
}

class Worker {
  final SendPort _commands;
  final ReceivePort _responses;
  final Map<int, Completer<Object?>> _activeRequests = {};
  int _idCounter = 0;
  bool _closed = false;

  Future<Object?> parseJson(String message) async {
    if (_closed) throw StateError('Closed');
    final completer = Completer<Object?>.sync();
    final id = _idCounter++;
    _activeRequests[id] = completer;
    _commands.send((id, message));
    return await completer.future;
  }

  static Future<Worker> spawn() async {
    // Create a receive port and add its initial message handler.
    final initPort = RawReceivePort();
    final connection = Completer<(ReceivePort, SendPort)>.sync();
    initPort.handler = (initialMessage) {
      final commandPort = initialMessage as SendPort;
      connection.complete((
        ReceivePort.fromRawReceivePort(initPort),
        commandPort,
      ));
    };

    // Spawn the isolate.
    try {
      await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort));
    } on Object {
      initPort.close();
      rethrow;
    }

    final (ReceivePort receivePort, SendPort sendPort) =
        await connection.future;

    return Worker._(receivePort, sendPort);
  }

  Worker._(this._responses, this._commands) {
    _responses.listen(_handleResponsesFromIsolate);
  }

  void _handleResponsesFromIsolate(dynamic message) {
    final (int id, Object? response) = message as (int, Object?);
    final completer = _activeRequests.remove(id)!;

    if (response is RemoteError) {
      completer.completeError(response);
    } else {
      completer.complete(response);
    }

    if (_closed && _activeRequests.isEmpty) _responses.close();
  }

  static void _handleCommandsToIsolate(
    ReceivePort receivePort,
    SendPort sendPort,
  ) {
    receivePort.listen((message) {
      if (message == 'shutdown') {
        receivePort.close();
        return;
      }
      final (int id, String jsonText) = message as (int, String);
      try {
        final jsonData = jsonDecode(jsonText);
        sendPort.send((id, jsonData));
      } catch (e) {
        sendPort.send((id, RemoteError(e.toString(), '')));
      }
    });
  }

  static void _startRemoteIsolate(SendPort sendPort) {
    final receivePort = ReceivePort();
    sendPort.send(receivePort.sendPort);
    _handleCommandsToIsolate(receivePort, sendPort);
  }

  void close() {
    if (!_closed) {
      _closed = true;
      _commands.send('shutdown');
      if (_activeRequests.isEmpty) _responses.close();
      print('--- port closed --- ');
    }
  }
}
相关推荐
行业探路者2 小时前
录音转二维码与视频二维码生成器的使用指南
人工智能·学习·音视频·二维码·产品介绍
2501_944441752 小时前
Flutter&OpenHarmony商城App用户中心组件开发
java·javascript·flutter
断剑zou天涯2 小时前
【算法笔记】有序表——跳表
笔记·算法
モンキー・D・小菜鸡儿2 小时前
Android 自定义浮动线条视图实现:动态视觉效果的艺术
android·java
Darkershadow2 小时前
蓝牙学习之亮度调节
学习·蓝牙·ble
2501_944446002 小时前
Flutter&OpenHarmony主题切换功能实现
开发语言·javascript·flutter
秋深枫叶红2 小时前
嵌入式第四十三篇——数据库
linux·数据库·学习·oracle
q行2 小时前
java学习日志--内部类
java·学习·内部类
BlackWolfSky2 小时前
鸿蒙中级课程笔记1—CodeGenie功能介绍
笔记·华为·鸿蒙