在 Flutter 应用中直接使用 Pigeon Native Interop:FFI 与 JNI 平台通信实战指南
【免费下载链接】packagesA collection of useful packages maintained by the Flutter team项目地址: https://gitcode.com/GitHub_Trending/pac/packages
Pigeon 是 Flutter 团队维护的代码生成工具,传统上它通过 MethodChannel 在 Flutter 与原生代码之间通信。而 Native Interop(原生互操作)则是一条更底层的路径:不经过消息通道,直接在应用中通过 Dart FFI 调用 Objective-C/Swift、通过 JNI 调用 Kotlin/Java。本指南以 Flutter 官方仓库中packages/pigeon/example/native_interop_app示例应用为主线,讲解如何在不编写插件的前提下,让应用直接与宿主平台的原生代码通信,并掌握代码生成、原生注册、Dart 调用与集成测试的完整闭环。
Native Interop 解决了什么问题
传统 Pigeon 生成的平台通道代码依赖 Flutter 引擎的消息路由机制:Dart 端通过BasicMessageChannel发送经过编解码的消息,原生端在插件注册器中注册 handler。整个过程工作良好,但要求平台逻辑必须托管在插件(Plugin)中。
Native Interop 特性改变了这一约束。正如示例应用的 README 所述,它演示了:
直接在应用(Application)中使用 Pigeon 的 Native Interop 特性(直接的 FFI 和 JNI 函数调用)进行平台通信,而非在插件中。
也就是说,你可以把原生实现直接写进 Android 的MainActivity或 iOS 的AppDelegate,Dart 代码绕过消息通道,通过 FFI/JNI 的桥接代码直接调用到宿主函数。这一能力对于需要在应用内直接访问系统 API、硬件能力或第三方原生 SDK 的场景尤其有价值。
从本仓库生成的桥接代码可以清楚看到两种路径的并存:lib/src/native_interop_example.g.dart中同时导入了 FFI 桥(native_interop_example.g.ffi.dart)和 JNI 桥(native_interop_example.g.jni.dart),并在运行时按平台选择调用方式。
示例应用的目录结构
packages/pigeon/example/native_interop_app是一个完整的 Flutter 应用工程,其关键组成部分如下:
| 目录/文件 | 作用 |
|---|---|
pigeons/native_interop_example.dart | Pigeon 输入定义:API 声明与@ConfigurePigeon互操作配置 |
lib/main.dart | 应用入口,演示如何在 Dart 侧选择 Native Interop 通道 |
lib/src/native_interop_example.g.dart | Pigeon 生成的 Dart 桥接代码(含 FFI/JNI 分派逻辑) |
lib/src/native_interop_example.g.ffi.dart | 由 swiftgen + ffigen 生成的 FFI 绑定(macOS/iOS) |
lib/src/native_interop_example.g.jni.dart | 由 jnigen 生成的 JNI 绑定(Android) |
android/app/src/main/kotlin/dev/flutter/pigeonnativeinteropapp/ | Android 原生端:MainActivity.kt注册实现,NativeInteropExample.g.kt为生成代码 |
ios/Runner/ | iOS 原生端:AppDelegate.swift注册实现,NativeInteropExample.g.swift为生成代码 |
integration_test/example_app_test.dart | 端到端集成测试 |
tool/pigeon/ | FFI/JNI 生成器(ffigen、jnigen)的独立配置脚本 |
test_driver/integration_test.dart | 集成测试驱动入口 |
工程依赖(见 pubspec.yaml)中包含三个关键的运行时互操作库:jni(JNI 桥)、objective_c(Objective-C 桥)和ffi(Dart FFI 基础库);开发依赖则包含pigeon(本地路径引用仓库根)、jnigen、ffigen、swiftgen、swift2objc等生成工具。
定义 API 与 Native Interop 配置
Pigeon 的输入文件 pigeons/native_interop_example.dart 是整个生成流程的源头。除了用@HostApi()声明一个最简单的宿主 API 外,关键在于@ConfigurePigeon注解:
import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( // (推荐)编译后的应用目录路径(即 pubspec.yaml 所在目录) appDirectory: './', dartOptions: DartOptions(), kotlinOptions: KotlinOptions( useJni: true, // 可选:搜索已编译本地类的路径(主要用于独立应用场景) jniClassPaths: <String>['build/app/tmp/kotlin-classes/release'], ), swiftOptions: SwiftOptions(useFfi: true, ffiModuleName: 'Runner'), ), ) @HostApi() abstract class NativeInteropExampleApi { void doSomething(); }这里的关键配置项及含义:
appDirectory: './':指向编译后的应用目录(存放pubspec.yaml的位置)。这是启用 Native Interop 生成的前提,Pigeon 需要它来定位宿主应用的可执行产物。kotlinOptions.useJni: true:为 Android 端启用 JNI 代码生成,Dart 将通过 JNI 直接调用 Kotlin 实现。kotlinOptions.jniClassPaths:可选参数,指定用于搜索已编译本地 Kotlin 类的目录。示例中指向build/app/tmp/kotlin-classes/release,这正是 Gradle 编译 release Kotlin 类后产出的目录。之所以需要它,是因为独立应用(非插件)无法依赖插件注册机制,Pigeon 需要直接解析这些类来生成 JNI 桥。swiftOptions.useFfi: true+ffiModuleName: 'Runner':为 Apple 平台(iOS/macOS)启用 FFI 生成,ffiModuleName指向宿主应用的可执行模块名。在 iOS 应用中这个模块名通常是 Runner。
API 本身极简——一个无参数、无返回值的doSomething()——但足以完整演示从 Dart 到 Kotlin/Swift 的调用闭环。
重新生成代码:一条命令全流程
示例 README 给出了更新生成代码的命令:
cd ../.. dart tool/generate.dart从example/native_interop_app目录上移两级即到达 Pigeon 包根目录(packages/pigeon),然后在包根目录执行生成脚本。该脚本入口为 tool/generate.dart,其实际生成逻辑位于 tool/shared/generation.dart 的generateExamplePigeons()函数,流程比表面看起来要复杂:
- 先生成普通示例:对
example/app/pigeons/下的 messages 定义执行常规 Pigeon 生成。 - 编译原生应用:调用
_compileNativeInteropExampleApp(),通过flutter build apk --config-only配置工程,然后执行 Gradle 任务:app:compileReleaseKotlin,产出build/app/tmp/kotlin-classes/release下的 Kotlin 编译产物。这一步在无 Android SDK 或 Java 环境时会自动跳过。 - 运行 Pigeon 生成:以
pigeons/native_interop_example.dart为输入,产出 Dart(lib/src/native_interop_example.g.dart)、Kotlin(NativeInteropExample.g.kt)和 Swift(NativeInteropExample.g.swift)三类代码。 - 生成 FFI/JNI 桥:分别执行 native_interop_example_ffigen_config.dart(基于 swiftgen/ffigen)与 native_interop_example_jnigen_config.dart(基于 jnigen),生成
lib/src/下的.g.ffi.dart与.g.jni.dart。 - 自动格式化:脚本默认对所有生成输出执行
dart format。
值得注意的是各生成器自身也有平台前置条件:FFI 生成只支持 macOS 宿主环境(配置脚本中通过Platform.isMacOS检查);JNI 生成则要求可用 Java 运行时(通过JAVA_HOME或java命令探测)。
原生端实现:在应用而非插件中注册
Android(Kotlin + JNI)
Android 侧的原生实现位于 MainActivity.kt。它直接在FlutterActivity中实现 API 并通过生成器注册,全程不涉及插件:
package dev.flutter.pigeonnativeinteropapp import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine private class PigeonApiImplementation : NativeInteropExampleApi { override fun doSomething() { // 真实应用中,这里实现原生平台逻辑(访问 Android 系统 API、 // 硬件特性或第三方原生 SDK) println("NativeInteropExampleApi.doSomething called from Dart") } } class MainActivity : FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) val api = PigeonApiImplementation() NativeInteropExampleApiRegistrar().register(api) } }注册入口NativeInteropExampleApiRegistrar().register(api)是 Pigeon 为 Native Interop 生成的特殊类,它不依赖 Flutter 的插件注册表,而是把实现实例登记到 JNI 桥可查询到的位置,Dart 端的 JNI 桥会通过NativeInteropExampleApiRegistrar().getInstance(...)直接取回该实例(参见生成的native_interop_example.g.dart中getInstance的实现)。
iOS / macOS(Swift + FFI)
Apple 平台侧的实现位于 AppDelegate.swift。应用代理遵循FlutterImplicitEngineDelegate协议,在隐式 Flutter 引擎初始化时注册实现:
import Flutter import UIKit private class PigeonApiImplementation: NativeInteropExampleApi { func doSomething() throws { // 真实应用中,这里实现原生平台逻辑(访问 iOS 系统 API、 // 硬件特性或第三方原生框架) print("NativeInteropExampleApi.doSomething called from Dart") } } @main @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) let api = PigeonApiImplementation() NativeInteropExampleApiSetup.register(api: api) } }生成的NativeInteropExampleApiSetup.register(api:)把实现登记到 FFI 桥,Dart 侧通过NativeInteropExampleApiSetup.getInstanceWithName(...)取回。FFI 绑定在生成时需要把 Swift 声明转换为 Objective-C 兼容接口(ios/Runner_objc_gen/NativeInteropExample.g.m),再经 ffigen 生成 Dart 侧绑定,因此示例工程中可见Runner-Bridging-Header.h与Runner.h等桥接头文件。
生成的原生代码形态
原生生成代码同样存在于仓库中可供对照:Android 为 NativeInteropExample.g.kt,iOS 为 NativeInteropExample.g.swift。此外,NativeInteropExample.kt 与 NativeInteropExample.swift 中还展示了异步 API 的两种等价写法(回调风格与协程/async 风格),供扩展示例 API 时参考。
Dart 端调用:按平台选择通信路径
应用入口 lib/main.dart 展示了 Dart 侧的调用方式:
final NativeInteropExampleApi _api = (Platform.isAndroid || Platform.isIOS || Platform.isMacOS) ? NativeInteropExampleApi.createWithNativeInteropApi() : NativeInteropExampleApi();逻辑非常直白:
- Android / iOS / macOS:调用
createWithNativeInteropApi()工厂方法,走 Native Interop 路径(JNI 或 FFI); - 其他平台(如 Web、Windows、Linux):回退到默认构造器
NativeInteropExampleApi(),使用传统 BasicMessageChannel 通道。
随后在initState中调用_api.doSomething(),成功与失败分别更新 UI 文本。失败分支捕获的是PlatformException——这说明 Native Interop 路径虽然底层是 FFI/JNI,但对外仍然保持着与 MethodChannel 一致的异常语义。
从生成的 native_interop_example.g.dart 可以看到工厂方法的内部机制:createWithNativeInteropApi()先调用NativeInteropExampleApiForNativeInterop.getInstance(),该方法按平台分派——Android 走 JNI 桥的NativeInteropExampleApiRegistrar().getInstance(...),iOS/macOS 走 FFI 桥的NativeInteropExampleApiSetup.getInstanceWithName(...);若平台不支持则抛出UnsupportedError,并提示使用默认构造器。拿到桥接实例后,doSomething()内部根据持有的是 JNI 还是 FFI 句柄选择对应调用路径,并将原生异常统一包装为PlatformException抛出。
底层编解码与数据转换
Native Interop 之所以能替代消息通道,是因为生成的代码内部实现了 Dart 与原生对象之间的直接转换。生成的native_interop_example.g.dart中包含两个编解码器:
_PigeonJniCodec(Android):在 Dart 基本类型与 JNI 对象间转换——int↔JLong、double↔JDouble、String↔JString、Uint8List/Int32List/Int64List/Float64List↔JByteArray/JIntArray/JLongArray/JDoubleArray,List/Map则递归转换为JList/JMap;Kotlin 侧的Unit(无返回值)通过反射取得kotlin/Unit.INSTANCE静态字段来编码。_PigeonFfiCodec(Apple):基于package:objective_c,在 Dart 值与 Foundation 对象间转换——NSNumber承载数值与布尔、NSString承载字符串、NSArray/NSDictionary承载容器、NSData承载字节数据。其中值得注意的两个细节:- 二进制类型通过
PigeonTypedData包装(内部记录类型标签 0~4,分别对应Uint8List/Int32List/Int64List/Float32List/Float64List); - 泛型容器中的数值通过
NumberWrapper包装类型信息(1=long、2=double、3=bool),以解决 ObjC 容器无法区分数值类型的问题。
- 二进制类型通过
这些代码由 Pigeon 自动生成,使用者无需手写,但理解其存在有助于排查跨语言数据转换问题。FFI 桥的生成范围还受native_interop_example_ffigen_config.dart中的白名单控制:只有NativeInteropExampleApi、PigeonError等少数类与枚举会被纳入绑定,NS前缀的 Foundation 类型则交给package:objective_c处理。
集成测试验证
仓库为示例应用提供了端到端测试 integration_test/example_app_test.dart:
void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('gets host language', (WidgetTester tester) async { await tester.pumpWidget(const MyApp()); await tester.pumpAndSettle(); expect(find.textContaining('Called doSomething() successfully!'), findsOneWidget); }); }测试通过IntegrationTestWidgetsFlutterBinding在真实设备或模拟器上启动应用,断言 Dart 调用doSomething()后成功文本出现,从而验证 JNI/FFI 通道真实打通。配合 test_driver/integration_test.dart 驱动,可通过flutter drive在目标平台上运行。由于三种平台(Android/iOS/macOS)共用同一测试逻辑,该测试天然覆盖了 JNI 与 FFI 两条调用链。
平台支持与适用前提
综合示例应用代码与生成逻辑,可以归纳出 Native Interop 的适用边界:
- 支持平台:Android(JNI)、iOS 与 macOS(FFI)。示例代码中通过
Platform.isAndroid || Platform.isIOS || Platform.isMacOS判断。 - 生成环境:FFI 绑定生成仅能在 macOS 上执行(依赖 Xcode SDK 与 swiftgen/ffigen 工具链);JNI 绑定生成需要 Java 运行时与已编译的 Android Kotlin 类产物。
- 前置构建:Pigeon 生成 JNI 桥前需要应用先完成 Kotlin 编译(
./gradlew :app:compileReleaseKotlin),因此首次生成前往往需要先执行一次flutter build apk类的构建以产出build/app/tmp/kotlin-classes/release。 - 其他平台回退:在不支持 Native Interop 的平台上,示例展示了回退到传统消息通道的兼容写法(默认构造器),保证 API 面一致。
小结
通过packages/pigeon/example/native_interop_app这个最小可运行示例,可以完整掌握 Pigeon Native Interop 的工程闭环:在@ConfigurePigeon中开启useJni/useFfi并指定应用目录与模块名,用dart tool/generate.dart一键产出 Dart/Kotlin/Swift 与 FFI/JNI 桥接代码,在MainActivity/AppDelegate中直接注册实现,最终在 Dart 侧通过createWithNativeInteropApi()完成调用。相比传统插件通道,这一方案让原生代码可以直接存在于应用本体中,同时仍保留PlatformException等一致的错误语义,是应用级平台通信的实用选择。若要深入探索,可进一步阅读仓库中 Pigeon 的 Native Interop 迁移指南 与 原生互操作指南,了解从插件迁移至该方案的完整路径。
【免费下载链接】packagesA collection of useful packages maintained by the Flutter team项目地址: https://gitcode.com/GitHub_Trending/pac/packages
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考