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