Flutter 行動與桌面應用程式可以使用 dart:ffi 函式庫來呼叫原生 C API。 FFI 代表 foreign function interface,外部函式介面。 類似功能的其他術語還包括 native interface(原生介面)與 language bindings(語言綁定)。

在您的函式庫或程式能夠使用 FFI 函式庫 綁定原生程式碼之前,必須確保 原生程式碼已載入,且其符號對 Dart 可見。 本頁重點說明如何在 Flutter 外掛程式或應用程式中 編譯、封裝與載入 Android 原生程式碼。

本教學將示範如何在 Flutter 外掛程式中 封裝 C/C++ 原始碼,並透過 Dart FFI 函式庫 在 Android 與 iOS 上進行綁定。 在此操作說明中,您將建立一個實作 32 位元加法的 C 函式, 並透過名為 "native_add" 的 Dart 外掛程式將其公開。

動態連結與靜態連結

#

原生函式庫可以以動態或靜態方式連結到應用程式中。 靜態連結的函式庫會嵌入到應用程式的可執行映像檔中, 並於應用程式啟動時載入。

來自靜態連結函式庫的符號可以透過 DynamicLibrary.executableDynamicLibrary.process 載入。

相較之下,動態連結的函式庫則會以獨立檔案或資料夾的形式 分發於應用程式內,並在需要時載入。 在 Android 上,動態連結函式庫會以一組 .so(ELF) 檔案的形式分發,每個架構一份。

動態連結函式庫可以透過 DynamicLibrary.open 載入到 Dart 中。

API 文件可參考 Dart API reference documentation

在 Android 上僅支援動態函式庫 (因為主要可執行檔為 JVM,無法進行靜態連結)。

建立 FFI 外掛程式

#

若要建立名為 "native_add" 的 FFI 外掛程式, 請依照下列步驟操作:

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

這會在native_add/src中建立一個包含 C/C++ 原始碼的外掛(plugin)。 這些原始碼會由各個作業系統建置資料夾中的原生建置檔案進行編譯。

FFI 函式庫只能綁定到 C 符號, 因此在 C++ 中這些符號需要標記為extern "C"

你也應該加上屬性來標示這些符號會從 Dart 參考, 以避免連結器在連結時最佳化(link-time optimization)時將這些符號移除。 __attribute__((visibility("default"))) __attribute__((used))

在 Android 上,native_add/android/build.gradle 會負責連結這些程式碼。

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

綁定(bindings)是透過 package:ffigen 產生的。

其他使用情境

#

平台函式庫

#

若要連結到平台函式庫, 請依照以下步驟操作:

  1. 在 Android 文件的 Android NDK Native APIs 清單中找到你想要的函式庫。這份清單列出了穩定的原生 API。

  2. 使用 DynamicLibrary.open 載入該函式庫。 例如,若要載入 OpenGL ES(v3):

    dart
    DynamicLibrary.open('libGLES_v3.so');

如果文件中有說明,您可能需要更新應用程式或套件的 Android manifest 檔案。

第一方函式庫

#

將原生程式碼(無論是原始碼或二進位檔)納入應用程式或套件的流程是相同的。

開源第三方

#

請依照 Android 文件中的將 C 和 C++ 程式碼加入您的專案說明, 將原生程式碼與原生程式碼工具鏈(CMake 或 ndk-build)的支援加入專案。

封閉原始碼第三方函式庫

#

若要建立包含 Dart 原始碼的 Flutter 套件(plugin),但以二進位形式發佈 C/C++ 函式庫,請依照下列步驟操作:

  1. 開啟您專案的 android/build.gradle 檔案。
  2. 將 AAR artifact 加入為相依套件。 不要將該 artifact 直接包含在您的 Flutter 套件中。應該從像 JCenter 這樣的套件庫下載。

Android APK 檔案大小(shared object 壓縮)

#

[Android 指南][Android guidelines]通常建議以未壓縮方式發佈原生 shared object,因為這樣實際上能節省裝置空間。 shared object 可以直接從 APK 載入,而不需要先在裝置上解壓縮到暫存位置再載入。 APK 在傳輸過程中會額外壓縮——因此您應該關注下載檔案大小。

Flutter APK 預設遵循這些指南,會壓縮 libflutter.solibapp.so——這會讓 APK 檔案本身較小,但實際佔用裝置空間較大。

第三方的 shared object 可以在其 AndroidManifest.xml 檔案中,透過 android:extractNativeLibs="true" 來改變這個預設設定,並停止壓縮 libflutter.solibapp.so 及任何使用者新增的 shared object。 若要重新啟用壓縮,請在 your_app_name/android/app/src/main/AndroidManifest.xml 中以如下方式覆寫設定。

xml
<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. -->

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