# 通道、Pigeon 与 FFI：跨到原生，但不按跳付费

> Flutter 提供三种触达原生代码的方式，成本和失败形态都不同。选对主要取决于一个数字（你跨越的频率）和一条规则（按帧批量，绝不按跳）。

- Canonical (HTML): https://kaguc.com/blog/flutter-native-channels-zh/
- Date: 2026-08-28


*English version: [Channels, Pigeon, and FFI: crossing to native without paying per tick](/blog/flutter-native-channels/)。这是[交易类 App 架构](/blog/trading-app-architecture-zh/)的 Flutter 实现篇，另见[跨平台的边界](/blog/cross-platform-boundary-zh/)。*

从 Dart 出去有三条路，而它们并不能互相替代。选得好不好，与其说取决于性能跑分，不如说取决于机制是否匹配你的跨越频率，以及你在另一侧需要什么。

## 三种机制

**平台通道**（`MethodChannel`、`EventChannel`）把消息经引擎送到平台代码。它是**异步**的，参数会经过编解码器**序列化**，并且会切换线程。这让它非常适合平台能力：权限、相机、生物识别、分享面板，这些调用本来就不频繁，而且工作本来就发生在平台那一侧。它的成本由每次调用的固定开销主导，而不是参数大小。

**Pigeon** 是通道之上的代码生成。你只声明一次接口，它生成带类型的 Dart 代码和平台代码。运行时成本和手写通道一样；你换来的是消灭了一整类缺陷：方法名拼错、参数形状不匹配、类型被静默转换。通道天然会招来这类问题，因为它两侧都是以字符串标识、且没有类型的。只要你的通道方法超过两三个，就该用它。

**`dart:ffi`** 直接调用 C。它是**同步**的，**没有序列化**，并且可以通过类型化数据与 Dart 共享内存。当你需要直接对接一个原生库的 API，或者需要低成本地搬运大块缓冲区时，它是唯一选择。它的坑恰好就是它的优点：同步调用运行在发起它的那个 isolate 上，所以一次慢的 FFI 调用会阻塞该 isolate；如果那是 UI isolate，你就掉帧了。截至 Flutter 3.47 / Dart 3.13（2026 年 8 月），打包原生侧的官方方式是 FFI package 里的构建钩子（`hook/build.dart`）——`flutter create --template=package_ffi`，自 Flutter 3.38 起即为推荐做法——它取代了以前为 C 代码逐平台写插件脚手架的方式。

## 决定一切的那个数字

跨越的成本是**按次调用**支付的。仅这一个事实就决定了大部分设计。

一条每秒 5000 条消息的数据流，如果逐条经通道封送，就是每秒 5000 次序列化往返加线程切换。它会比全部用 Dart 做还慢，尽管原生代码“更快”。你为了省下一些解码，付出了几千次跨越开销。

同一条流，如果由原生解码并合并，Dart **每帧读取一次**快照，那就是每秒 60 次跨越，每次带一批数据。同样的数据量，边界成本少了两个数量级。

所以规则就是[管线那篇](/blog/realtime-data-pipeline-zh/)里那条，用 Flutter 的说法重述一遍：**按帧跨越，绝不按跳跨越。** 把原生接口设计成少数几个搬运批量数据的粗操作，而不是一串单条事件，也不是一组属性读取。

## 高频数据源的一个具体形态

如果你确实要做原生接收，行得通的接口长这样：原生持有套接字，负责解码、检测丢包、维护一张当前值表；Dart 只问“自我上次查看以来变了什么”。

```dart
final class QuoteBridge {
  QuoteBridge(this._lib);
  final NativeLib _lib;

  /// 把变化的行情拷进一个可复用缓冲区。每帧跨越一次。
  int drainInto(Float64List out) => _lib.drain(out.address, out.length);
}
```

这里有两个性质很关键。缓冲区是**复用**的，不是每次调用都分配，所以你不会每秒制造 60 次垃圾。而且这个调用返回的是写入预分配区域的**数量**，而不是跨边界构造对象，因为从原生代码逐元素地分配 Dart 对象，会把你本想避开的按条成本又请回来。

然后，只要 FFI 调用有可能变慢，就把它挪出 UI isolate：

```dart
// 长时间或阻塞的工作属于它自己的 isolate。
final decoded = await Isolate.run(() => heavyDecode(bytes));
```

如果原生代码需要反过来调用 Dart，比如一个完成回调、或数据源的一次推送，使用 `NativeCallable`。它给你一个能安全重入 Dart isolate 的函数指针。不要绕过它，从任意原生线程直接调用 Dart。

## 内存归属必须写清楚

FFI 去掉了序列化边界，同时也去掉了你原本隐式依赖的自动内存管理。对每一个跨线的缓冲区，都要决定并写明：**谁分配，谁释放。**

站得住的模式有两种：

- **Dart 分配，原生填充。** Dart 在整个生命周期内持有一个类型化数据缓冲区并传下去。这是最简单的，也是我的默认选择，因为生命周期没有任何歧义。
- **原生分配，Dart 显式释放。** 把它包装成“释放不是可选项”的形式：作用域分配用 `using((arena) { ... })`，或者挂一个 `NativeFinalizer`，让被丢弃的 Dart 对象自动释放它的原生对应物。

这里的失败不是你在测试中能抓到的崩溃，而是一次缓慢的泄漏，表现为内存曲线在使用一小时后持续爬升，而那恰好是你从不测试的会话长度。

## EventChannel 没有背压

这一点值得单独说，因为它常常出乎意料：一个 `EventChannel`，如果喂入速度快过 Dart 的消费速度，就会排队。它没有内置机制让消费者说“慢一点”，所以一个快速的原生生产者会变成无上限的缓冲，然后变成内存问题。（这个症状早在 flutter/flutter#33640 就有记录：原生侧持续向 `EventChannel` 喂数据，内存以稳定速率上涨。）

如果原生是高频流的生产者，就要**在原生一侧做合并**，让 Dart 主动拉取；或者按你控制的节奏推送（每帧一次），而不是按数据到达的节奏推送。在这里施加背压是生产者的职责，因为传输层不会替你做。

## 简短的选择建议

- 偶发的平台能力 → 用 **Pigeon 生成的通道**。类型安全是白得的，调用频率无关紧要。
- 只有少数几个调用，不想引入代码生成 → 直接用 `MethodChannel` 也可以，把方法面保持得小一些。
- 大块缓冲区，或必须直接对接的原生库 → 用 **FFI**，归属写明确，慢操作放到辅助 isolate。
- 高频流 → 原生做合并，Dart 经 FFI **每帧拉取一次**。绝不要逐条消息走通道。
- 任何能在 Dart 里以可接受成本完成的事 → **就在 Dart 里做。** 那条边界是一笔长期的税，交给调试、崩溃上报和构建复杂度，它应当由一次测量来证明其合理，而不是由“原生更快”的直觉。

最后一条是最常见的错误。调用栈会在边界处中断，崩溃上报需要为两侧分别上传符号表，而一次复现从此牵涉两套工具链。等性能剖析告诉你该跨的时候再跨，不要因为原生听起来更快就跨。


## 参考

*API 与渲染器相关断言按 Flutter 3.47.0 / Dart 3.13（2026 年 8 月）核对。*

- Flutter 文档，[Writing custom platform-specific code](https://docs.flutter.dev/platform-integration/platform-channels)——`MethodChannel` / `EventChannel`、编解码器与线程规则。
- pub.dev 上的 [`pigeon`](https://pub.dev/packages/pigeon)——在平台通道之上做带类型的代码生成。
- Dart 文档，[C interop using dart:ffi](https://dart.dev/interop/c-interop)——`@Native`、`NativeCallable`、`NativeFinalizer`，以及 `Arena` / `using`。
- Flutter 文档，[Bind to native code using FFI](https://docs.flutter.dev/platform-integration/bind-native-code)；Dart 文档，[Hooks](https://dart.dev/tools/hooks)——`hook/build.dart` 与 `package_ffi` 模板。
- Dart 文档，[Isolates](https://dart.dev/language/isolates)——isolate 之间的消息是拷贝的；`Isolate.run` 则转移其结果。另见 [`TransferableTypedData`](https://api.dart.dev/dart-isolate/TransferableTypedData-class.html)。
- [flutter/flutter#33640](https://github.com/flutter/flutter/issues/33640)——原生生产者喂 `EventChannel` 快过 Dart 消费时的稳定内存增长。
- J. Waldo, G. Wyant, A. Wollrath, S. Kendall，["A Note on Distributed Computing"](https://scholar.harvard.edu/files/waldo/files/waldo-94.pdf)，Sun Microsystems Laboratories TR-94-29，1994——地址空间之间的边界无法被抹平；延迟与部分失败按每次调用计价。

---

*上一篇：[Flutter 面对高频数据](/blog/flutter-highfreq-state-zh/) · 下一篇：[图表与自绘](/blog/flutter-chart-rendering-zh/)*

