FFI(Foreign Function Interface)— 是 Dart 语言的一种机制,由 dart:ffi 包提供,允许直接调用原生 C 库中的函数,无需 Kotlin、Swift 或 Java 的中间层。开发者加载动态库(Android 上为 .so,iOS 上为 .dylib,Windows 上为 .dll),声明 C 函数的签名,并像普通 Dart 函数一样调用它们。根据 Dart API Reference (2025),FFI 将跨语言调用的开销降低到 0.1 微秒,比通过 Method Channel 快数十倍。
要点
FFI(Foreign Function Interface)— 是一种机制,允许编程语言调用用其他语言编写的函数。在 Dart 和 Flutter 的上下文中,FFI 意味着能够直接从 Dart 代码调用 C/C++ 库中的函数,而无需在 Java(Android)或 Swift/Objective-C(iOS)中编写平台代码。
dart:ffi 包出现在 Dart 2.12(2021)中,从那时起它已成为将 Flutter 与原生代码集成的关键工具。在 dart:ffi 出现之前,从 Dart 调用 C 函数的唯一方法是 Method Channel — 一种通过 JSON 序列化在 Dart 和原生端之间传输消息的异步机制。FFI 的工作方式不同:Dart 代码直接访问 C 库的内存,通过 原生 ABI(应用程序二进制接口)调用函数,无需序列化和上下文切换。
FFI 在性能至关重要的场景中尤为需要:图像处理(OpenCV)、音频(FFmpeg)、加密(OpenSSL)、机器学习(TensorFlow Lite)和数据库(SQLite)。在所有这些情况下,Method Channel 会产生不可接受的延迟,而 FFI 提供了与原生 C/C++ 代码相当的性能。dart:ffi 库还支持内存操作:分配、释放和指针管理。
Method Channel 异步工作:Dart 向原生代码发送消息,原生代码处理它并发送回结果。每次调用都需要将参数序列化为 Map,通过队列传输并反序列化。每次调用需要 0.5-5 毫秒。FFI 同步且无需序列化 — C 函数调用只需 0.01-0.1 微秒。相差 50-500 倍,这对高频操作至关重要。
使用 dart:ffi 包括三个阶段:加载库、声明签名和调用函数。每个阶段都使用 Dart 的严格类型系统,从而最大限度地减少运行时错误。
在第一阶段,通过 DynamicLibrary 类加载动态库。库可以按名称(libxyz.so、libxyz.dylib、xyz.dll)或按完整路径加载。Dart 会自动在系统的标准路径中搜索库。DynamicLibrary 提供 lookupFunction 方法,该方法根据符号名称将 Dart 函数与 C 函数关联起来。
在第二阶段,声明一个 Dart 函数,其类型注解与 C 签名对应。为此使用 dart:ffi 中的特殊类型:Int32、Float、Double、Pointer、NativeFunction、Handle 等。lookupFunction 注解接受两个泛型参数:Dart 函数的类型(在 Dart 中的样子)和原生 C 函数的类型(在 C 中声明的方式)。
在第三阶段,生成的 Dart 函数像普通函数一样被调用。参数直接传递,结果立即返回。如果 C 函数通过指针修改内存,Dart 可以通过 Pointer 类读取这些更改。内存管理在 C 端仍然是开发人员的责任 — dart:ffi 不管理 C 中由 malloc 分配的内存。
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// C 函数声明:int add(int a, int b)
typedef AddNative = Int32 Function(Int32, Int32)
typedef AddDart = int Function(int, int)
void main() {
final lib = DynamicLibrary.open('libcalculator.so')
final AddDart add = lib
.lookupFunction<AddNative, AddDart>('add')
print(add(5, 3)) // 8
}
在此示例中,add 是一个 C 函数,接受两个 int 并返回 int。typedef AddNative 使用 dart:ffi 类型描述 C 签名,而 AddDart 描述此函数在 Dart 中的样子。lookupFunction 将它们关联起来并返回一个可以像普通函数一样调用的 Dart 函数。
dart:ffi 提供了一套与 C 类型对应的类型。每种类型都有固定的大小以及在 Dart 和 C 之间的转换规则。理解类型的对应关系对于 FFI 的正确运行至关重要 — 类型大小或符号的错误可能导致应用程序崩溃。
| C 类型 | dart:ffi 类型 | Dart 类型 | 大小(字节) |
|---|---|---|---|
| int | Int32 | int | 4 |
| long | Int64 | int | 8 |
| float | Float | double | 4 |
| double | Double | double | 8 |
| char* | Pointer<Int8> | Pointer | 8(指针) |
| void* | Pointer<Void> | Pointer | 8(指针) |
| struct | Pointer<T>(Struct) | Pointer | 取决于字段 |
为了处理 C 字符串(char*),dart:ffi 使用 Pointer<Int8>。从 Dart String 到 C char* 及反向的转换通过 toNativeUtf8(来自 ffi 包)和 fromUtf8 方法完成。使用后通过 calloc.free 释放 C 字符串以防止内存泄漏非常重要。
dart:ffi 支持将 C 结构体声明为继承 Struct 的 Dart 类。结构体的字段使用 @Int32()、@Float()、@Array() 等注解声明。字段的大小和偏移量根据平台的 ABI 自动计算。Pointer<Point> 可以从返回结构体指针的 C 函数获取,或在 Dart 中通过 calloc 分配。
// C 结构体:typedef struct { int x; int y; } Point;
final class Point extends Struct {
@Int32()
external int x
@Int32()
external int y
}
// 调用返回 Point* 的 C 函数
typedef CreatePointNative = Pointer<Point> Function(Int32, Int32)
typedef CreatePointDart = Pointer<Point> Function(int, int)
final Pointer<Point> p = createPoint(10, 20)
print('x:${p.ref.x},y:${p.ref.y}')
calloc.free(p) // 释放内存
Point 类继承 Struct 并使用 @Int32() 注解声明字段 x 和 y。生成的 C 代码在内存中将具有完全相同的字段布局。Pointer.ref 通过 getter 和 setter 提供对结构体字段的访问。
让我们看一个更复杂的示例 — 与 C 库集成以计算 SHA256 哈希。这是一个典型任务,FFI 在此比 Method Channel 提供显著的性能优势。
OpenSSL 库提供 SHA256 函数,用于计算字符串的哈希值。通过 dart:ffi,我们可以直接调用它,无需编写 Java 或 Swift 包装器。这是一个示例,展示了 FFI 如何允许在 Flutter 中重用现有的 C 库。
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// 签名:unsigned char* SHA256(
// const unsigned char *d, size_t n, unsigned char *md)
typedef Sha256Native = Pointer<Uint8> Function(
Pointer<Uint8>, Size, Pointer<Uint8>)
typedef Sha256Dart = Pointer<Uint8> Function(
Pointer<Uint8>, int, Pointer<Uint8>)
String sha256(String input) {
final lib = DynamicLibrary.open('libcrypto.so')
final Sha256Dart sha256Fn = lib
.lookupFunction<Sha256Native, Sha256Dart>('SHA256')
final inputPtr = input.toNativeUtf8()
final outputPtr = calloc(Uint8)(32) // SHA256 = 32 字节
sha256Fn(inputPtr, input.length, outputPtr)
final digest = outputPtr.asTypedList(32)
final hex = digest.map((b) => b.toRadixString(16)
.padLeft(2, '0')).join()
calloc.free(inputPtr)
calloc.free(outputPtr)
return hex
}
在此示例中,sha256 函数加载 libcrypto.so 库,找到 SHA256 符号并使用指向输入和输出数据的指针调用它。toNativeUtf8 将 Dart String 转换为 C 字符串(分配内存),asTypedList 允许读取结果的字节数组。内存在使用后释放 — 这是防止泄漏的强制步骤。
ffi 包提供 calloc 函数以分配与 C 兼容的内存。分配的内存必须通过 calloc.free 释放,否则将发生泄漏。要自动管理内存,可以使用 ffi 包中的 Arena 类,它在调用 arena.release() 时释放其中分配的所有内存。这在有大量临时分配时特别方便。
尽管 FFI 功能强大,但它有一些在 Design Flutter 应用架构时必须考虑的限制。主要限制涉及类型安全、内存管理和平台兼容性。
FFI 在运行时不会检查类型。如果 C 函数期望指针但传递了数字,应用程序将因段错误而崩溃。建议使用 FFIgen — 一种基于 C 头文件(.h 文件)生成类型安全 Dart 包装器的工具。FFIgen 分析 C 函数的声明并创建具有正确类型的 Dart 代码,从而消除编写代码阶段的错误。
动态库的名称和路径在不同平台上有所不同:Android/Linux 上为 libxyz.so,iOS/macOS 上为 libxyz.dylib,Windows 上为 xyz.dll。对于跨平台库,通过 dart:io(Platform.isAndroid、Platform.isIOS)或诸如 package:ffi 的抽象使用条件编译。建议创建一个工厂方法,返回当前平台的正确库。
FFI 不管理 C 端的内存。如果 C 函数通过 malloc 分配内存,必须通过 free 释放,否则将发生泄漏。Dart 中没有用于 C 内存的垃圾回收器。建议:始终在分配内存的同一方法中释放内存,或使用 Arena 进行分组释放。
FFI 调用在同一个线程中执行 Dart 代码。长时间的同步操作(超过 10 毫秒)会阻塞 UI 线程并导致帧丢失。对于长时间操作,应在隔离区(Isolate)中调用 C 函数,或确保 C 函数在后台线程启动工作并通过 Port 或回调通知 Dart。
// 隔离区中的 FFI 用于长时间操作
import 'dart:isolate'
Future<String> computeHash(String input) async {
final port = ReceivePort()
await Isolate.spawn((SendPort sendPort) {
final result = sha256(input) // FFI 调用
sendPort.send(result)
}, port.sendPort)
return await port.first as String
}
将 FFI 调用转移到隔离区可确保 UI 线程不会被阻塞。但是,在隔离区之间传输大量数据需要复制内存。对于大型缓冲区(大于 10 MB),最好使用 ShareMemory 或内存映射文件。
常见问题
FFI 直接、同步且无需序列化地调用 C 函数 — 延迟 0.01-0.1 微秒。Method Channel 通过 JSON 序列化异步工作,延迟 0.5-5 毫秒。FFI 适用于高性能操作,Method Channel 适用于简单的平台 API 调用。
不能直接调用,dart:ffi 只支持 C 函数。要调用 C++,需要创建一个带有 extern "C" 的 C 包装器(作为 C 符号导出的入口点)。C++ 类需要额外的层来将方法调用转换为 C 函数。
FFI 不支持异常 — 如果 C 函数返回错误代码,需要手动检查。建议将 FFI 调用包装在 Dart 的 try-catch 中,并检查 C 函数的返回代码。关键错误(段错误)无法捕获。
FFI 无法与需要复杂 Java(JNI)或 Objective-C(Message Dispatch)初始化的库一起使用。例如,UIKit 和 Android Views 无法通过 FFI 访问。局限性与 FFI 在 C ABI 级别工作有关,而这些 API 需要特定的运行时环境。
是的,C 库为每个目标平台分别编译。对于 Android,为不同的 ABI(armeabi-v7a、arm64-v8a、x86_64)构建 .so。对于 iOS — 通用 .dylib(arm64)。对于 Windows — .dll。Flutter 在构建时自动打包正确版本的库。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。