Android 上使用 dart:ffi 调用本地代码

Flutter 移动版可以使用 dart:ffi 库来调用本地的 C API。 FFI 代表 外部功能接口。类似功能的其他术语包括本地接口语言绑定

你必须首先确保本地代码已加载,并且其符号对 Dart 可见,然后才能在库或程序使用 FFI 库绑定本地代码。本页主要介绍如何在 Flutter 插件或应用程序中编译、打包和加载本地代码。

本教程演示了如何在 Flutter 插件中捆绑 C/C++ 源代码,并使用 Android 和 iOS 上的 Dart FFI 库绑定它们。在本示例中,你将创建一个实现 32 位的加法 C 函数,然后通过名为 “native_add” 的 Dart 插件暴露它。

动态链接 vs 静态链接

本地库可以动态或静态地链接到应用程序中。一个静态链接库会被嵌入到应用程序的可执行映像中,并在应用程序启动时加载。

静态链接中的符号可以使用 DynamicLibrary.executableDynamicLibrary.process 来加载。

相比之下,动态链接库则分布在应用程序中的单独的文件或文件夹中,并按需加载。在 Android 上,动态链接库作为一组 .so(ELF 可执行与可链接格式)文件分发,每个架构各有一个。

动态链接库在 Dart 中可以通过 DynamicLibrary.open 加载。

Dart dev 频道中的 API 已经可用: Dart API 参考文档.

步骤 1:创建插件

如果你已经有一个插件,跳过这步。

如果要创建一个名为 “native_add” 的插件,你需要这么做:

$ flutter create --platforms=android,ios --template=plugin native_add
$ cd native_add

步骤 2:添加 C/C++ 源码

你需要让 Android 和 iOS 构建系统知道本地代码的存在,以便代码可以被编译并链接到最终的应用程序中。

你可以将特定于 Android 的源代码添加到 android 文件夹并修改 CMakeLists.txt 文件。同时,你可以在 Gradle 中指向 ios 文件夹,这样的话就可以为 iOS 和 Android 设定不同的资源。

FFI 库只能与 C 符号绑定,因此在 C++ 中,这些符号添加 extern C 标记。还应该添加属性来表明符号是需要被 Dart 引用的,以防止链接器在优化链接时会丢弃符号。

在 Android 中,你需要创建一个 CMakeLists.txt 文件用来定义如何编译源文件,同时告诉 Gradle 如何去定位它们。在项目根目录下,运行如下代码:

cat > android/CMakeLists.txt << EOF
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 )
EOF

最后,添加一个 externalNativeBuild 到你的 android/build.gradle 文件中。示例如下:

android {
  // ...
  externalNativeBuild {
    // Encapsulates your CMake build configurations.
    cmake {
      // Provides a relative path to your CMake build script.
      path "CMakeLists.txt"
    }
  }
  // ...
}

步骤 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();

请注意,在 Android 上,本地库的名称是定义在 CMakeLists.txt 中的(见上文),但在 iOS 上,它将使用插件的名称。

你可以通过使用库的句柄来解析 native_add 符号:

final int Function(int x, int y) nativeAdd = nativeAddLib
    .lookup<NativeFunction<Int32 Function(Int32, Int32)>>('native_add')
    .asFunction();

现在,你可以调用它了。在自动生成的 “example” 项目(example/lib/main.dart)中演示它。

// Inside of _MyAppState.build:
        body: Center(
          child: Text('1 + 2 == ${nativeAdd(1, 2)}'),
        ),

其他的用例

Platform library

平台库

要链接到平台库,请按照如下说明:

  1. 在 Android 文档的 Android NDK Native APIs 列表中找到所需的库。它列出了稳定的本地 API。

  2. 使用 DynamicLibrary.open 加载库。示例:加载 OpenGL ES (v3):

    DynamicLibrary.open('libGLES_v3.so');
    

如果文档中有说明,你还需要根据说明更新 Android 应用程序或插件的清单文件。

第一方库

对于应用程序或插件,以源代码或二进制形式包含本机代码的过程是相同的。

开源三方库

遵循 Android 文档中的 添加 C 和 C++ 代码到项目 来添加本地代码和对本地代码工具链的支持(CMake 或 ndk-build)。

闭源三方库

要创建包含 Dart 源代码,但以二进制形式分发 C/C++ 库的 Flutter 插件,请按照如下说明:

  1. 打开你项目的 android/build.gradle 文件。

  2. 添加 aar 工件添加为依赖。 不要在你的 Flutter package 中导入工件。对应的,它需要在一个仓库中下载,比如 JCenter。

Android APK 尺寸(共享对象压缩)

Android 指南 通常建议分发未压缩的本地共享对象,因为这种做法实际上可以节省设备空间。共享对象可以直接从 APK 加载,而不是将它们解压到设备上的临时位置然后再加载。 APK 是在传输过程中额外打包的 - 这就是为什么你应该查看下载的文件尺寸。

Flutter APK 文件默认情况下不遵循这些指导原则来压缩 libflutter.solibapp.so,这会导致 APK 体积更小,但在设备上体积更大。

来自第三方的共享库可以使用其 AndroidManifest.xml 中的 android:extractNativeLibs="true" 更改此默认设置,来停止压缩 libflutter.solibapp.so 和任何用户添加的共享库。要重新启用压缩,请按照如下方式重写你的 your_app_name/android/app/src/main/AndroidManifest.xml 文件。

@@ -1,5 +1,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.example.your_app_name">
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.example.your_app_name" >
     <!-- io.flutter.app.FlutterApplication is an android.app.Application that
          calls FlutterMain.startInitialization(this); in its onCreate method.
          In most cases you can leave this as-is, but you if you want to provide
          additional functionality it is fine to subclass or reimplement
          FlutterApplication and put your custom class here. -->
@@ -8,7 +9,9 @@
     <application
         android:name="io.flutter.app.FlutterApplication"
         android:label="your_app_name"
-        android:icon="@mipmap/ic_launcher">
+        android:icon="@mipmap/ic_launcher"
+        android:extractNativeLibs="true"
+        tools:replace="android:extractNativeLibs">