Flutter 与 Dart 直连原生 API 的三条路线

对于已有 C ABI 动态库的情况,可以在 Direct Native Interop 与传统 Dart FFI 之间按性能和维护成本进行选择,而对于系统级 API、UI 生命周期相关能力及跨平台公共能力,仍应优先使用插件或 Platform Channel。Dart Direct Native Interop 去掉了一部分显式的动态库装载、符号查找和函数包装步骤,但这并不意味着它会自动解决原生对象的生命周期管理、动态库打包、线程阻塞以及内存释放等底层工程问题。

三条路线各有边界

三条路线在调用入口、数据边界、延迟特征与维护成本上存在差异。Platform Channel 适合低频的跨语言消息传递;FFI 和 Direct Native Interop 主要面向高性能的原生调用。

路线 调用入口 数据边界 延迟特征 维护成本
Platform Channel Dart 调用 invokeMethod,原生端处理 onMethodCall 传递可编码的 Map、List 等值,不传原生指针 异步消息模型,每次调用都有编解码与调度成本 需要维护通道协议及多平台实现
传统 Dart FFI DynamicLibrary.lookupFunction,配合 NativeFunction.asFunction 直接使用 C ABI 的整数、指针、结构体等类型 接近原生调用,但同步调用会阻塞当前 isolate 需要管理动态库、符号、ABI 和绑定代码
Direct Native Interop @Native 注解配合 external 声明 与传统 FFI 相同,仍受 C ABI 约束 热路径不经过消息编解码,也无需业务代码手工查找符号 依赖目标 SDK、符号可见性和动态库构建方式

以 Android 的 MethodChannel 为例,原生插件通常实现 FlutterPlugin 和 MethodCallHandler:在 onAttachedToEngine 中创建通道,在 onMethodCall 中分发调用,并在 onDetachedFromEngine 中清理资源。

后两种路线共享 FFI 与 ABI 基础。Direct Native Interop 不是对 FFI 的替代,而是更声明式的符号绑定方式。传统 FFI 通常只在初始化时查找一次符号,因此它相对 Direct 模式的单次调用收益未必很大,是否值得采用仍应由目标设备和真实负载验证。

直调依赖符号与 ABI

Direct Native Interop 的可用性和声明形式取决于项目锁定的 Dart/Flutter SDK 版本。下面采用 NativeFunction 描述原生签名的形式;复制前应在目标 SDK 中编译验证,不能混用其他版本的注解写法。

import 'dart:ffi' as ffi;

@Native<ffi.NativeFunction<ffi.Int32 Function(ffi.Int32, ffi.Int32)>>(
  symbol: 'native_add',
)
external int nativeAdd(int a, int b);

这一调用链从符号解析开始。external 声明提供 Dart 侧的调用签名,@Native 将其映射到原生符号,NativeFunction 则记录对应的 C ABI 签名。动态库必须已被装载,符号也必须真实存在且对外可见;把 .so 放进安装包并不代表系统一定能按名称解析到其中的符号。

参数与返回值按照 ABI 规则传递:Int32 等类型按 ABI 规则转换,指针传递内存地址,结构体则要求字段宽度、对齐、填充和调用约定完全一致。C++ 库通常需要增加稳定的 C ABI 包装层,避免把 C++ 名称修饰、异常模型和编译器差异暴露给 Dart。

同步调用会阻塞当前 isolate。原生函数通常返回结果或状态码,而不是自动生成 Dart 异常,因此需要建立明确的状态码到 Dart 异常的映射,必要时通过输出参数返回复杂结果。

原生回调需要注册句柄,并在使用期间保持相关 Dart 对象存活;注销回调后,还要释放回调注册占用的资源。原生内存则遵循“谁分配、谁释放”的规则,不能依赖 Dart 垃圾回收器回收。

传统 FFI 的典型链路是 DynamicLibrary.open → lookupFunction → Dart 可调用对象;NativeFunction.asFunction 是显式转换手段之一。Direct 模式通过 @Native 与 external 声明完成符号映射并提供可调用入口,但底层仍依赖同一套 FFI 与 ABI 机制。ffigen 可以根据头文件生成绑定,减少手写代码,却不能替代 ABI 审核、符号导出和动态库打包。

高频调用优先验证

已有 C/C++ 库可优先评估直连;计算密集型逻辑、音视频处理和加密算法尤其适合小粒度、高频率的调用。Platform Channel 的异步消息与序列化成本并不适合这类高频交互,而 Direct 模式可以减少消息编解码和手工符号查找。

这里的重点是减少桥接开销,而不是让算法本身变快。耗时计算仍应放在工作 isolate,避免阻塞 UI isolate;能够批量处理的数据,也不宜拆成大量细小调用。

平台 API 仍需桥接层

当 Java/Kotlin、Swift/Objective-C API 没有暴露为可解析的 C ABI 时,@Native 与 external 无法直接表达这些对象模型及其生命周期。此类需求应继续使用 Platform Channel 或插件,也可以增加 JNI、Objective-C 包装层,再由包装层提供稳定的 C ABI。

与 Activity、View、UI 生命周期绑定的能力尤其不适合裸直调;原生主动调用 Dart 的需求也仍应由通道模型处理。 需要多个 Flutter 目标平台共同复用的公共能力,应保持统一接口并实现为插件,避免各平台分别维护一套直调实现。

已经自研 DartNative 类通信基础设施的团队,可以将其作为定制路线进行对照,但它与 Direct Native Interop 不是同一种机制:前者解决通信基础设施问题,后者主要改变 C ABI 符号的声明和绑定方式。

上线前补齐工程护栏

直调只改变调用链,不会改变原生 ABI 契约。上线前至少应核对以下项目:

  • 原生契约:检查参数宽度、有符号性、结构体对齐与填充、调用约定,以及 C/C++ 声明与实现是否一致。
  • 符号与装载:确认符号可见性、导出规则、装载顺序和调用时机,不能只验证文件是否被打包。
  • 动态库发布:按目标 ABI 与设备架构准备 .so、.dylib、.dll 等对应产物,并验证安装包中的实际架构。
  • 回调与内存:明确回调注册、注销和引用存活规则;核对原生内存的分配与释放函数,避免跨分配器释放。
  • 错误与线程:建立错误码映射,并隔离可能阻塞的同步调用。
  • 平台与版本:确认目标 Dart/Flutter SDK 是否具备所需能力。Web 没有可直接链接的本机动态库 ABI,不能沿用同一套直调实现,需要独立适配或回退方案。

用最小闭环完成选型

选型应先选择一个无 UI 依赖、输入输出明确的 C ABI 函数,实现最小闭环。在此基础上,可以同时实现 Direct 声明和传统 FFI 绑定;如果需要建立性能基线,再为同一函数补一个 MethodChannel 版本。

随后,在目标 ABI 对应的真实设备上记录端到端耗时、内存占用和回调稳定性,并确认该函数能够稳定运行,而不要只看空函数或开发环境结果。数据达到预期后,再逐步扩大调用范围,并按照目标 SDK 的官方安装与平台配置文档完成发布环境验证。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容