Appearance
Native 原生线程与 Dart Isolate 桥接 (FFI 零拷贝与 NativePort)
回到总览:混合开发与桥接
相关模块:PlatformChannel 深度解析:MethodChannel / EventChannel / BasicMessageChannel · Dart Isolate、compute() 与无共享多核并发
一句话定义
Native 线程与 Dart Isolate 桥接是指通过 dart:ffi (Foreign Function Interface) 直接在 Dart 中调用 C/C++ 原生函数;利用 Pointer 物理内存零拷贝访问以及 Dart_PostCObject API,实现 C++ 后台工作线程向 Dart UI Isolate 异步跨语言通信的高性能桥接架构。
代码索引
对应 Lab:dart-ffi-demo · ffi_demo.dart
为什么需要
- 为什么在处理高频图像处理、音视频解码或加密算法时,
dart:ffi性能远超PlatformChannel?- 一句话答:
PlatformChannel依赖内存序列化与切回 Native 主线程,开销极大;dart:ffi可以直接将 Native C++ 内存指针(Pointer<Uint8>)传递给 Dart,无任何序列化开销,实现真正的物理内存零拷贝 (Zero-Copy)。
- 一句话答:
- C++ 任意后台线程如何向 Dart UI Isolate 安全回调发送消息?
- 一句话答:Dart Isolate 堆内存是非线程安全的;C++ 后台线程绝对不能直接修改 Dart 堆内存,必须通过
Dart_PostCObject(sendPort, &cObject)将数据发给 Dart 的ReceivePort,由 Dart 事件循环安全唤醒。
- 一句话答:Dart Isolate 堆内存是非线程安全的;C++ 后台线程绝对不能直接修改 Dart 堆内存,必须通过
底层机制
1. dart:ffi 零拷贝与 Dart_PostCObject 线程安全回调
对应 Lab:dart-ffi-demo
图注补充:DartUI: Dart UI Isolate (线程 A);CPPWorker: C++ 后台工作线程 (线程 B);ReceivePort: Dart ReceivePort;FFI: 1. 传递 NativePort ID (receivePort.sendPort.nativePort);CPPWorker: 2. 异步启动 C++ 后台解码任务 (不阻塞 UI);CPPWorker: 3. C++ 线程执行密集图像解码;ReceivePort: 4. 调用 Dart_PostCObject(nativePort, data);DartUI: 5. 跨线程塞入 Dart EventLoop 队列;DartUI: 6. 唤醒 receivePort.listen() 监听器,刷新 UI!。
Android / Flutter / Web / Backend 对照
| 桥接维度 | dart:ffi | PlatformChannel | Android JNI (Java ↔ C++) |
|---|---|---|---|
| 内存拷贝 | 物理零拷贝 (Pointer 指针直接访问) | 需要二进制序列化拷贝 | 需要 GetByteArrayElements 拷贝 |
| 线程安全 | 依靠 Dart_PostCObject 安全压队列 | 强制切回 Native 主线程 | 需要 AttachCurrentThread |
| 适合场景 | 音视频处理、OpenCV、加密解密 | 调用系统 API、组件通信 | 调用 C/C++ 动态链接库 |
代码示例:C++ 线程回调 Dart
cpp
// C++ 侧代码
#include "dart_api_dl.h"
void ExecuteAsyncWork(int64_t send_port) {
std::thread([send_port]() {
// 后台 C++ 线程做耗时计算
int result = 42;
Dart_CObject response;
response.type = Dart_CObject_kInt32;
response.value.as_int32 = result;
// 安全将结果投递给 Dart Isolate 的 EventLoop
Dart_PostCObject_DL(send_port, &response);
}).detach();
}Dart 侧必须配套 ReceivePort + listen 才能收到这条消息——两侧配对缺一不可:
dart
// Dart 侧代码
import 'dart:ffi';
import 'package:ffi/ffi.dart';
final int Function(int sendPort) _executeAsyncWork =
lib.lookup<NativeFunction<Int32 Function(Int64)>>('ExecuteAsyncWork').asFunction();
void startNativeWork() {
// 1. 创建 ReceivePort,拿到 nativePort 传给 C++
final port = ReceivePort();
port.listen((message) {
// 4. 消息回到 Dart 事件循环,在这里更新 UI / 业务状态
print('native result: $message'); // 输出: native result: 42
});
// 2. 把 SendPort 的 nativePort ID 传给 C++ 后台线程
_executeAsyncWork(port.sendPort.nativePort);
}- 可能执行顺序:Dart 创建
ReceivePort→ 把nativePort传给 C++ → C++ 后台线程计算 →Dart_PostCObject投递 → 消息进入 Dart 事件循环 →listen回调触发。 - 可能输出:
native result: 42(且回调一定发生在 Dart 事件循环线程,不会并发侵入 Dart 堆)。 - 观察重点:
sendPort.nativePort是 C++ 侧可用的整数句柄;listen回调里访问的是 Dart 堆,安全的前提正是"C++ 只发数据、不碰 Dart 内存"。
常见误配、事故后果与排障
1. 事故:C++ 线程直接调用 Dart 函数 / 修改 Dart 对象
- 误配原因:为了"省事",把 Dart 侧回调函数指针(如
NativeFunction包装)直接存到 C++ 全局并在后台线程调用。 - 后果:Dart VM 的 isolate 堆非线程安全,后台线程并发触碰堆内存会直接崩溃(Segfault /
Cannot invoke native callback on a thread not attached to the VM)。 - 排障与修法:一律通过
Dart_PostCObject把数据发回 Dart 事件循环处理;若 C++ 侧确实需要同步调用 Dart,必须先Dart_NewNativePort注册 + 由 Dart 线程消费,而不是裸调用。
2. 事故:ReceivePort 泄漏,C++ 持续回调导致内存膨胀
- 误配原因:创建
ReceivePort后忘记在页面销毁时port.close(),C++ 侧仍持有nativePort持续投递。 - 后果:消息无限累积、端口对象无法回收,内存持续增长;页面已退出但回调还在触发(类似回调泄漏)。
- 排障与修法:在
dispose/ 生命周期退出时显式port.close(),并让 C++ 侧在收到停止信号后停止投递;必要时用弱引用 / 引用计数管理端口生命周期。
3. 事故:把大对象(图片帧 / 视频 buffer)走 Dart_PostCObject 逐帧拷贝
- 误配原因:高频回调时每次都把整块数据拷贝进
Dart_CObject。 - 后果:拷贝开销吃掉 FFI 省下的性能,掉帧甚至 OOM。
- 排障与修法:大块数据用
Dart_PostCObject_DL投递指针 + 长度(配合dart:ffi的Pointer视图零拷贝读取),或改用NativePort+ 预分配缓冲池,只在必要时拷贝。
对应实验
| Lab | 说明 | 源码 |
|---|---|---|
| dart-ffi-demo | dart:ffi 调 C 函数 + malloc/asTypedList 零拷贝 | ffi_demo.dart |
复习检查题
在 Flutter 中,为什么 C++ 的后台线程绝对不能直接修改 Dart 的对象或内存?
答:因为 Dart VM 采用的是 Isolate 隔离体模型,每个 Isolate 拥有一块独立的、单线程访问的堆内存,其 GC 和对象分配完全没有加锁。如果允许 C++ 的并发后台线程直接修改 Dart 堆内存,会直接破坏 Dart VM 的内存一致性导致崩溃。因此必须通过
Dart_PostCObject将数据发送给 Dart 的事件队列(Event Queue),由 Dart 线程自身取出处理。dart:ffi为什么能够实现物理内存的“零拷贝 (Zero-Copy)”?答:因为
dart:ffi允许 Dart 直接操作 C/C++ 在 Native 堆上分配的物理内存指针(Pointer<T>)。Dart 可以直接通过指针读写这块 Native 内存(如pointer.cast<Uint8>().asTypedList(length)),而无需像 PlatformChannel 那样在 Dart 堆和 Native 堆之间做内存复制与序列化。
速记
- FFI 核心优势:直接操作 Pointer 指针,实现物理内存零拷贝。
- 线程安全回调:C++ 后台线程回调 Dart 必须通过
Dart_PostCObject压入 EventQueue。 - 最佳场景:高频音视频解码、图像处理、加密算法首选
dart:ffi。