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 參考文件.

在 Android 平台只有動態函式庫可以使用(因為在 JVM 環境無法靜態連結)。

建立 FFI 外掛

如果要建立一個名為 “native_add” 的外掛,你需要這麼做:

$ flutter create --platforms=android,ios,macos,windows,linux --template=plugin_ffi native_add
$ cd native_add

C/C++ 原始碼會被建立至 native_add/src。這些原始碼在不同平台建立時會生成在不同平台的建立檔案夾。

FFI 函式庫只能繫結 C 語言的符號,所以 C++ 語言的符號會被標記為 extern "C"

FFI 函式庫只能與 C 符號繫結,因此在 C++ 中,這些符號新增 extern C 標記。還應該新增屬性來表明符號是需要被 Dart 引用的,以防止連結器在最佳化連結時會丟棄符號。 __attribute__((visibility("default"))) __attribute__((used)).

在 Android 上 native_add/android/build.gradle 負責關聯這些程式碼。

原生程式碼會從 lib/native_add_bindings_generated.dart 被 Dart 呼叫。

程式碼由 package:ffigen 生成。

其他的用例

平台函式庫

要連結到平台函式庫,請按照如下說明:

  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">

Other Resources

To learn more about C interoperability, check out these videos: