一、背景
虽然flutter官方的插件有很多,但是有一部分是比较稀缺的。
例如我们在进行hid设备通信时,发现官方的hid包已经两年没更新了,而flutter的迭代速度很快,基于此我们需要自己开发一个插件。
1.1 是否支持
通过查阅flutter官方文档,我们发现flutter官方提供了一个dart:ffi(Foreign Function Interface: 外部功能接口)库来调用本地的 C API。
二、集成源码
我推荐使用方式二集成c源码,它不需要我们人为的去编写转换文件。
2.1 方法一
生成一个flutter的plugin,然后在这个plugin里对应平台(ios/android)目录下添加C源代码,并根据平台指定的方式进行编译并链接到最终的程序中。
2.1.1 生成plugin
输入以下命令
#其中platplatforms是支持的平台,每个平台会生成对应的目录和相关配置文件等,template是指创建flutter项目的类型,这里我们选择plugin即插件的形式。 flutter create --platforms=android,ios --template=plugin native_add |
2.1.2 添加C/C++源码
作为示例,我们在ios目录下的Classes路径下添加一个native_add.cpp文件(CocoaPods 不允许源码处于比 podspec 文件更高的目录层级,但是 Gradle 允许你指向 ios 文件夹,所以我们偏向于将源代码放到ios目录下)
native_add.cpp 内代码如下:
#include <stdint.h> extern "C" __attribute__((visibility("default"))) __attribute__((used)) int32_t native_add(int32_t x, int32_t y) { return x + y; } |
一个实现 32 位的加法 C 函数
备注:
FFI库只能与C符号绑定,因此在C++中,这些符号添加 extern C 标记。还应该添加属性来表明符合是需要被Dart引用的,以防止链接器在优化链接时会丢弃符号。
android编译示例
针对android平台的话,你需要创建一个 CMakeLists.txt 文件用来定义如何编译源文件,同时告诉 Gradle 如何去定位它们。
cmake_minimum_required(VERSION 3.4.1) # for example add_library( native_add # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). ../ios/Classes/native_add.cpp ) |
最后,添加一个 externalNativeBuild 到你的 android/build.gradle 文件中。示例如下:
android { // ... externalNativeBuild { // Encapsulates your CMake build configurations. cmake { // Provides a relative path to your CMake build script. path "CMakeLists.txt" } } // ... } |
这样最终会在android平台编辑生成一个动态链接库.so文件
2.1.3 使用 FFI 库绑定本地代码
接下来,我们需要在 lib/native_add.dart文件中编写一些代码,将本地源代码转换成Dart代码
首先,你需要创建一个 DynamicLibrary 来处理本地代码。这一步在 iOS 和 Android 之间有所不同:
import 'dart:ffi'; // For FFI import 'dart:io'; // For Platform.isX final DynamicLibrary nativeAddLib = Platform.isAndroid ? DynamicLibrary.open('libnative_add.so') : DynamicLibrary.process(); //静态链接中的符号可以使用 DynamicLibrary.executable 或 DynamicLibrary.process 来加载。 //动态链接库在 Dart 中可以通过 DynamicLibrary.open 加载。 |
在 Android 上,库的名称是定义在 CMakeLists.txt 中的(见上文),但在 iOS 上,它将使用插件的名称(flutter create 命令最后的名字)。
接着,我们通过使用库的句柄来解析native_add 符号,将本地方法转化为dart可以使用的方法。
final int Function(int x, int y) nativeAdd = nativeAddLib .lookup<NativeFunction<Int32 Function(Int32, Int32)>>('native_add') .asFunction(); |
2.1.4 调用方法
为了验证集成是否成功,我们可以在plugin项目内的example子项目(这是自动生成的一个app类型的项目)内的lib/main.dart内尝试调用这个方法:
// Inside of _MyAppState.build: body: Center( child: Text('1 + 2 == ${nativeAdd(1, 2)}'), ), |
不过,大部分情况下,我们会把这个plugin引入到一个正常的app项目中:
举个例子,比如我们新建一个flutter app 项目,名字叫flutter_app,它与native_add这个plugin处在同一层级下,我们就可以在flutter_app项目的pubspec.yaml文件添加它对native_add的依赖:
dependencies: native_add: path: ../native_add/ |
flutter_app项目main.dart文件,引入native_add包:
import "package:native_add/native_add.dart"; |
最后,就可以直接调用集成的本地方法了。
2.2 方法二
使用FFI plugin进行c代码调用
关于FFI plugin
FFI plugin是专门为绑定本地源代码而设计出来的,常规plugin虽然也可以支持,3.0之后对C源代码功能的支持ffi plugin会更强大,所以我们如果只是调用C代码,不需要平台SDK API的话,可以考虑使用FFI plugin。
2.2.1 创建项目
输入命令行
flutter create --platforms=android,ios --template=plugin_ffi hello |
2.2.2 添加C/C++源码以及相关编译配置文件
创建完成后,我们观察一下FFI plugin 项目的目录结构,对比常规plugin,主要有以下几点不同:
- 本地的源代码文件和CmakeFile.txt文件现在统一放到项目的src目录下
- ios平台目录Classes下面的源文件存在,只是引入了src下面的源代码
- android平台build.gradle 文件中externalNativeBuild属性中cmake的路径也是指向src中的CmakeFile.txt。
// Relative import to be able to reuse the C sources.information. #include "../../src/hello.c" |
android { externalNativeBuild { cmake { path "../src/CMakeLists.txt" } } } |
2.2.3 源代码的编译与绑定
项目中的pubspec.yaml 提供了如下配置选项:
plugin: platforms: android: ffiPlugin: true ios: ffiPlugin: true |
意思是利用ffiPlugin去为各个不同的平台编译源代码,并且绑定了二进制文件集成到flutter应用中去,你需要哪些平台都需要体现在这个配置项中。
2.2.4 加载库与转换为dart方法(主要)
ffiPlugin项目为我们提供了一种方式,让我们可以利用源代码根据一定的转化规则自动生成dart的方法,这个是通过ffigen.yaml文件与ffigen命令去完成的:
flutter pub run ffigen --config ffigen.yaml |
ffigen.yaml内容如下:
# Run with `flutter pub run ffigen --config ffigen.yaml`. name: HelloBindings description: | Bindings for `src/hello.h`. Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`. output: 'lib/hello_bindings_generated.dart' headers: entry-points: - 'src/hello.h' include-directives: - 'src/hello.h' preamble: | // ignore_for_file: always_specify_types // ignore_for_file: camel_case_types // ignore_for_file: non_constant_identifier_names comments: style: any length: full |
生成后的文件如下,可以看出,它是根据头文件中定义的本地方法自动生成了dart代码,这个代码文件中有一个HelloBindings类,里面的方法与头文件中的方法存在映射关系。
... import 'dart:ffi' as ffi; class HelloBindings { /// Holds the symbol lookup function. final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) _lookup; /// The symbols are looked up in [dynamicLibrary]. HelloBindings(ffi.DynamicLibrary dynamicLibrary) : _lookup = dynamicLibrary.lookup; int sum( int a, int b, ) { return _sum( a, b, ); } late final _sumPtr = _lookup<ffi.NativeFunction<ffi.IntPtr Function(ffi.IntPtr, ffi.IntPtr)>>( 'sum'); late final _sum = _sumPtr.asFunction<int Function(int, int)>(); ... } |
2.2.5 调用方法
代码示例如下
const String _libName = 'hello'; /// The dynamic library in which the symbols for [HelloBindings] can be found. final DynamicLibrary _dylib = () { if (Platform.isMacOS || Platform.isIOS) { return DynamicLibrary.open('$_libName.framework/$_libName'); } if (Platform.isAndroid || Platform.isLinux) { return DynamicLibrary.open('lib$_libName.so'); } if (Platform.isWindows) { return DynamicLibrary.open('$_libName.dll'); } throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}'); }(); /// The bindings to the native functions in [_dylib]. final HelloBindings _bindings = HelloBindings(_dylib); int sum(int a, int b) => _bindings.sum(a, b); |
在这个文件里,我们还是要通过DynamicLibrary来加载本地库文件,再将实例传到类的构造方法中,调用sum方法时在HelloBindings类中实现了具体的转换细节。
三、其他集成
3.1 平台库
要链接到平台库,请按照如下说明:
1. 在 Xcode 中,打开 Runner.xcworkspace
。
2. 选择目标设备。
3. 在 Linked Frameworks and Libraries 中点击 +。
4. 选择要链接的系统库。
3.2 已编译的动态库(第三方库)
-
在 Xcode 中打开
yourapp/macos/Runner.xcworkspace
。-
拖动您已经预编译的
libyourlibrary.dylib
到您的Runner/Frameworks
。 -
点击
Runner
然后进入Build Phases
标签。-
拖动
libyourlibrary.dylib
到Copy Bundle Resources
列表。 -
在
Embed Libararies
下,检查Code Sign on Copy
。 -
在
Link Binary With Libraries
下,设置状态为Optional
。(我们使用动态链接,不需要静态链接)
-
-
点击
Runner
然后进入General
标签页。-
拖动
libyourlibrary.dylib
到 Frameworks, Libararies and Embedded Content 列表中。 -
选择 Embed & Sign。
-
-
点击
Runner
然后进入Build Settings
标签页。-
在
Search Paths
部分,配置Library Search Paths
确保libyourlibrary.dylib
的路径包括在内。
-
-
-
编辑
lib/main.dart
文件。-
使用
DynamicLibrary.open('libyourlibrary.dylib')
来动态链接符号表。 -
在 widget 的某个地方调用您的本地代码。
-
-
运行
flutter run
然后检查您的本地方法的调用结果。