一、背景

虽然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 已编译的动态库(第三方库)

  1. 在 Xcode 中打开 yourapp/macos/Runner.xcworkspace

    1. 拖动您已经预编译的 libyourlibrary.dylib 到您的 Runner/Frameworks

    2. 点击 Runner 然后进入 Build Phases 标签。

      1. 拖动 libyourlibrary.dylib 到 Copy Bundle Resources 列表。

      2. 在 Embed Libararies 下,检查 Code Sign on Copy

      3. 在 Link Binary With Libraries 下,设置状态为 Optional。(我们使用动态链接,不需要静态链接)

    3. 点击 Runner 然后进入 General 标签页。

      1. 拖动 libyourlibrary.dylib 到 Frameworks, Libararies and Embedded Content 列表中。

      2. 选择 Embed & Sign

    4. 点击 Runner 然后进入 Build Settings 标签页。

      1. 在 Search Paths 部分,配置 Library Search Paths 确保 libyourlibrary.dylib 的路径包括在内。

  2. 编辑 lib/main.dart 文件。

    1. 使用 DynamicLibrary.open('libyourlibrary.dylib') 来动态链接符号表。

    2. 在 widget 的某个地方调用您的本地代码。

  3. 运行 flutter run 然后检查您的本地方法的调用结果。

(1)添加Frameworks

(2)检查Build Phases 

(3)设置General

(4)Build Settings

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐