零拷贝
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