零拷贝

Dart的零拷贝与Java的零拷贝指向的是完全不同的应用场景。熟悉Java的朋友知道Java 的零拷贝主要解决内核态(Kernel Space)与用户态(User Space)之间反复的数据拷贝开销。而Dart 的零拷贝是为了省去 Isolate 之间内存绝对隔离带来的通信开销。

Dart提供两种方式的零拷贝:

  • Isolate.exit()(所有权转让)
  • TransferableTypedData(字节流零拷贝,共享内存缓冲区)

Isolate.exit()

试想你要解析一个巨大的JSON字符串(如50MB),你启用一个Worker Isolate来解析它:Main Isolate将JSON字符串的文件路径或HTTP URL发给 Worker Isolate,后者将解析结果(bigMap)传回。此类应用场景正是Dart零拷贝的用武之地。我们先来看一下零拷贝方案的时序图。

sequenceDiagram
	  autonumber
    participant M as Main Isolate
    participant FS as File System / Network
    participant W as Worker Isolate (Background)

    Note over M: 监听到大数据处理请求
    M->>+W: Spawn Isolate (传递文件路径/URL)
    Note right of M: 主线程保持流畅,不加载大文件

    rect rgb(240, 248, 255)
        Note over W: 开始后台作业
        W->>FS: 读取文件或发起 HTTP 请求
        FS-->>W: 返回数据流/字符串
        W->>W: jsonDecode() 解析数据
    end

    Note over W: 解析完成,准备回传结果(bigMap)
    W->>M: Isolate.exit(resultPort, bigMap)
    Note over W: Worker 立即销毁,释放堆内存
    
    M-->>M: 接收 Map 对象 (内存所有权转让)
    Note over M: 后续处理(如更新UI)

请注意该时序图的第5步,我们使用的是 Isolate.exit(resultPort, bigMap) 而非 resultPort.send(bigMap);虽然它们都能将数据(bigMap) 传回 Main Isolate,但底层的内存处理机制完全不同。

特性SendPort.send(message)Isolate.exit(port, message)
内存行为物理拷贝 (Copying)所有权转让 (Transfer)
时间复杂度O(n)(与数据量成正比)O(1)(瞬间完成)
Worker 状态继续运行,除非手动 kill立即终止 (Terminate)
内存峰值高(主从 Isolate 同时持有副本)极低(内存块所有权转移)

虽然 Isolate.exit() 极快,但它有一个硬性限制:它是该 Isolate 发布的“遗言”。如果你需要 Worker Isolate 持续运行(例如一个长期驻留的 WebSocket 监听器),你必须使用 send(),因为 exit() 会直接杀掉当前 Isolate。

示例

// ex761.dart
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';

void main() async {
  print('main: start');
  // Simulate a large json file
  final jsonpath = '${Directory.current.path}/bin/ch07/ex761.json';
  final receivePort = ReceivePort();
  Isolate.spawn(parseBigJson, (jsonpath, receivePort.sendPort)); // 1

  receivePort.listen((bigMap) {
    // 6
    if (bigMap is Map) {
      print('main: result.length=${bigMap.length}');
      if (bigMap.length < 1000) {
        print('main: result=$bigMap');
      }
      receivePort.close();
    }
  });
}

void parseBigJson((String, SendPort) message) async {
  if (message case (String jsonpath, SendPort resultPort)) {
    print('worker: paring...');
    final content = await File(jsonpath).readAsString(); // 2-3
    final bigMap = jsonDecode(content); // 4
    print('worker: parse done');
    Isolate.exit(resultPort, bigMap); // 5
  }
}

事实上Isolate.run() 内部就是通过Isolate.exit() 将结果回传给创建它的Isolate的,所以我们通常是直接调用 Isolate.run()来处理此类问题,代码更为健壮与简洁。

// ex761a.dart
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';

void main() async {
  print('main: start');
  // Simulate a large json file
  final jsonpath = '${Directory.current.path}/bin/ch07/ex761.json'; // 1

  final bigMap = await Isolate.run(() async {
    print('worker: paring...');
    final file = File(jsonpath);
    final content = await file.readAsString();
    final result = jsonDecode(content);
    print('worker: parse done');
    // The result is sent using Isolate.exit, which means it's sent to main
    // isolate without copying.
    return result;
  });

  print('main: result.length=${bigMap.length}');
  if (bigMap.length < 1000) {
    print('main: result=$bigMap');
  }
}

TransferableTypedData(字节流零拷贝)

如果你需要在两个都保持存活的 Isolate 之间传递大数据(例如 Uint8List),Isolate.exit() 就不适用了。这时可以使用 TransferableTypedData,它允许你在不拷贝字节数组的情况下,将内存访问权跨 Isolate 传递。

// ex762.dart
import 'dart:isolate';
import 'dart:typed_data';

void main() async {
  final mainReceivePort = ReceivePort();
  await Isolate.spawn(worker, mainReceivePort.sendPort); // 1

  // 准备数据并包装
  final Uint8List massiveData = Uint8List(100 * 1024 * 1024); // 100MB
  final transferable = TransferableTypedData.fromList([massiveData]); // 3

  mainReceivePort.listen((message) {
    if (message is SendPort) {
      var workerSendPort = message;
      print('Main 发送 100MB 数据...');
      workerSendPort.send(transferable); // 4
      // 发送后 massiveData 在 Main Isolate 中将不再可用
    } else {
      print(message); // 8
      mainReceivePort.close(); // 8a
    }
  });
}

void worker(SendPort mainSendPort) {
  final receivePort = ReceivePort();
  mainSendPort.send(receivePort.sendPort); // 2

  receivePort.listen((message) {
    final transferable = message as TransferableTypedData;
    // 获取原始字节流(零拷贝)
    final Uint8List bytes = transferable
        .materialize() // 5 materialize
        .asUint8List(); // 6 asUint8List
    mainSendPort.send('Worker 收到 ${bytes.length / 1024 ~/ 1024} MB'); // 7
  });
}
sequenceDiagram
    autonumber
    participant M as Main Isolate
    participant W as Worker Isolate
    participant RAM as Shared Memory (C Heap)

    M->>+W: Isolate.spawn(worker, mainSendPort)
    
    Note over W: 初始化 ReceivePort
    W->>M: mainSendPort.send(workerSendPort)
    
    Note over M: 准备 100MB Uint8List
    M->>RAM: TransferableTypedData.fromList([massiveData])
    Note right of RAM: 数据从 Dart Heap 移动到可跨界访问的 C Heap

    M->>W: workerSendPort.send(transferable)
    Note over M: massiveData 在此变为不可用 (Empty)

    rect rgb(240, 248, 255)
        Note over W: 收到消息 (transferable)
        W->>RAM: transferable.materialize()
        Note right of RAM: 字节流映射到 Worker 的内存地址
        W->>W: 得到 Uint8List bytes
    end

    W->>M: mainSendPort.send("Worker 收到 100 MB")
    M-->>M: 打印消息并关闭 ReceivePort

a. 握手 (Steps 2-3): 由于 Isolate.spawn() 只能让主 Isolate 拿到 Worker 的句柄,反向通信需要 Worker 先通过 mainSendPort 把自己的 workerSendPort 发送回去。这是建立双向通信的标准做法。

b. 内存转移 (Step 4 & 6): TransferableTypedData.fromList 是核心。它将数据从 Dart 的普通堆(Heap)中移出,放到一个可以被多个 Isolate 访问的底层缓冲区(通常是 C 堆)。

  • Main 丢失:一旦 send(transferable) 执行,主 Isolate 就不再拥有这块数据。
  • Worker 获得:Worker 调用 materialize() 后,这块内存被“绑定”到 Worker 空间。

c. 零拷贝的本质: 数据本身在物理内存(RAM)中并没有被复制,只是访问权限从 Main 转移到了 Worker。

Reference

  • https://dart.dev/language/isolates
  • https://api.dart.dev/dart-isolate/Isolate-class.html