jni 1.0.0版本最近發布了。這是一個很好的機會,可以分享我過去幾年在原生互通方面積累的一些經驗教訓。

JNI 0.14/0.15 和 1.0.0 版本之間存在一些重大變更,因此如果您之前使用過 JNI 和 JNIgen,升級時可能會遇到一些額外的問題。本文的撰寫源自於我在遷移過程中遇到的 AI 代理輸出錯誤問題,這讓我非常惱火。

遷移到 [email protected]

我發現最快的方法是先註解掉所有原生整合程式碼,然後再重新建置 Android 專案並重新產生綁定。

一些 API 的名稱已經更改(恢復)為更合理的名稱,例如, toDartString()現在變成toString() ,或用於設定屬性的方法變成了setter_service.onReplyListener(_listener!);現在是_service.onReplyListener = _listener!; 寫)。還有一些令人煩惱的更改,例如構造函數重寫更改了數字,例如,Intent.new$2現在可能是Intent.new$12` …

此外,新的建議綁定組態定義方式不再是 YAML 文件,而是一個簡單的 Dart 腳本。我非常喜歡這種新方法,因為它隱藏了程式碼生成的底層機制,您只需在生成的程式碼中加入一些預處理和後處理邏輯。看來未來的慣例是將這些腳本放在tool/目錄下,然後使用dart run tool/jnigen.dartdart run tool/swiftgen.dart來執行它們。

您可以查看簡單 Flutter 應用程式的範例遷移提交:

  • 從 0.15 到 1.0.0

  • 從 0.14 到1.0.0

以下我將分享過去幾年在使用 jnigen 和 swiftgen 時學到的一些經驗教訓。最近幾個月,我更新了一些軟體包:

jnigen(Android)

快速回顧:

  • 您可以使用 jnigen 為明確和編譯後的 Java/Kotlin 程式碼產生 Dart 綁定。

  • 在生成綁定之前,您需要至少建立一次 Android 應用程式。 [^building]

  • 您可以同時包含專案特定的類別以及 Android SDK 類型。

  • 有一些內建的常用 Android 工具輔助程序,現在已移轉到package:jni_flutter 中

生成器腳本( tool/jnigen.dart

範例生成器腳本,用於建立一個包含兩個類別和一個回調介面的插件(回調模式將在下一節中解釋):

import 'dart:io';
import 'package:jnigen/jnigen.dart';

void main(List<String> args) {
  final packageRoot = Platform.script.resolve('../');
  generateJniBindings(Config(
    outputConfig: OutputConfig(
      dartConfig: DartCodeOutputConfig(
        path: packageRoot.resolve('lib/src/my_plugin.g.dart'),
        structure: OutputStructure.singleFile,
      ),
    ),
    androidSdkConfig: AndroidSdkConfig(
      addGradleDeps: true,
      androidExample: 'example',     // Points to example app for Gradle resolution
    ),
    sourcePath: [packageRoot.resolve('android/src/main/java/')],
    classes: [
      'com.example.MyCallback',       // List ALL classes to bind
      'com.example.MyPlugin',
    ],
  ));
}

回呼:使用 Kotlin 介面

如果你想從原生程式碼向 Dart 接收資料,我發現的方法是定義一個回調介面。然後在 Dart 端,你可以用更友善的 API(例如StreamController來封裝它。通常情況下,直接暴露回調就足夠了。

定義一個專用的 Kotlin 介面。 jnigen 會產生一個型別安全的implement()方法和$Mixin

// Generates BrightnessCallback.implement($BrightnessCallback(...))
@Keep
interface BrightnessCallback {
    @Keep
    fun onBrightnessChanged(brightness: Int)
}

然後是 Dart:

final callback = BrightnessCallback.implement(
  $BrightnessCallback(
    onBrightnessChanged: (brightness) { /* ... */ },
    onBrightnessChanged$async: true,  // Non-blocking (listener pattern)
  ),
);
native.startObserving(callback);

取得 Context 和 Activity 的存取權限 - package:jni_flutter

存取 Android 上下文 ( androidApplicationContext ) 和目前 Activity 的方式有一些小的變化。現在它屬於package:jni_flutter 。此外,要取得目前 Activity,您需要傳遞目前的engineId

final engineId = PlatformDispatcher.instance.engineId;
if (engineId == null) {
  print('Error: Engine ID is null');
  return;
}

final activity = androidActivity(engineId);
if (activity == null) {
  print('Error: Activity is null');
  return;
}
activity.as(a.Activity.type).startActivityForResult(intent, 1);

鑄件

若要將JObject強制轉換為所需的類型,請使用as()函數並傳入產生的類型:

final brightnessMonitor = native.getBrightnessMonitor();
final brightness = brightnessMonitor.as(ScreenBrightnessMonitor.type).brightness;

陣列

雖然有點麻煩,但一旦你掌握了,你就明白了:

final array = JArray.of<JString>(JString.type, ["[email protected]".toJString()]);

$async: true標誌

我發現我基本上總是把回調函數的參數設為async: true 。雖然有一些專門講線程的文件,但我不確定它是否是最新的。

使用@Keep進行註釋

ProGuard/R8 會移除未引用的類別。請為 jnigen 綁定的每個類別、介面、屬性和方法加入註解:

@Keep
class ScreenBrightnessMonitor(private val context: Context) {
    @get:Keep          // For Kotlin properties, use @get:Keep
    val brightness: Int
        get() = /* ... */

    @Keep
    fun startObserving(callback: BrightnessCallback) { /* ... */ }
}

重新產生綁定時出現問題

有時,即使修改了 Kotlin 程式碼並使用 Gradle 重新建置,jnigen 產生器仍可能拋出類似Unexpected end of input (at character 1)錯誤。我的解決方法是停用快取後重新執行 Gradle 建置。

cd android
./gradlew :your_plugin_name:assembleDebug --no-daemon --console=plain --refresh-dependencies --rerun-tasks
cd ..
dart run tool/jnigen.dart

記憶體管理

使用 JNI 綁定時,必須記住每次實例化時都會引用本機 Java 物件。

總體假設是,您無需手動管理它們。一旦所有指向某個物件的參考(無論是在 Java 或 Dart 中)都消失,Java 的垃圾回收器 (GC) 就可以回收該物件。類似地,JObject 會為其全域參考附加一個原生終結器。因此,當 Dart 的 GC 回收它們時,底層的 Java 引用也會被釋放。

然而,有時您可能需要更明確地控制物件的生命週期。請閱讀相關文件以了解更多關於引用管理的資訊。若要手動釋放 JNI 全域引用,請在 Dart 端呼叫 `.release()

void dispose() {
  native.stopObserving();
  callback?.release();
  native.release();
}

swiftgen(iOS)

Swiftgen 目前還不穩定,但我使用至今取得了一些成功。 Swift 程式碼需要與 Objective-C 相容,而 Swiftgen 則透過swift2objcffigen處理與 Dart 的橋接。

我發布了名為 screen_brightness_monitor 的軟體包,它使用 swiftgen 進行 iOS 綁定。

生成器腳本( tool/swiftgen.dart

與 jnigen 類似,我建議使用 Dart 腳本進行設定。以下是一個包含一個類別和一個回調協定的插件範例:

import 'dart:io';
import 'package:ffigen/ffigen.dart' as fg;
import 'package:logging/logging.dart';
import 'package:swiftgen/swiftgen.dart';

Future<void> main() async {
  final logger = Logger('swiftgen');
  logger.onRecord.listen((record) {
    stderr.writeln('${record.level.name}: ${record.message}');
  });

  final packageRoot = Platform.script.resolve('../');

  // Resolve SDK path/version manually:
  final sdkPath = (await Process.run('xcrun', [
    '--sdk', 'iphoneos', '--show-sdk-path',
  ])).stdout.toString().trim();
  final sdkVersion = (await Process.run('xcrun', [
    '--sdk', 'iphoneos', '--show-sdk-version',
  ])).stdout.toString().trim();

  await SwiftGenerator(
    target: Target(
      triple: 'arm64-apple-ios$sdkVersion',
      sdk: Uri.directory(sdkPath),
    ),
    inputs: [
      ObjCCompatibleSwiftFileInput(
        files: [
          packageRoot.resolve('ios/Classes/MyWidget.swift'),
        ],
      ),
    ],
    output: Output(
      module: 'my_plugin',
      dartFile: packageRoot.resolve('lib/src/my_plugin_ios.g.dart'),
      objectiveCFile: packageRoot.resolve('ios/Classes/my_plugin.m'),
    ),
    ffigen: FfiGeneratorOptions(
      objectiveC: fg.ObjectiveC(
        interfaces: fg.Interfaces(
          include: (decl) => decl.originalName == 'MyWidget',
        ),
        protocols: fg.Protocols(
          include: (decl) => decl.originalName == 'MyCallback',
        ),
      ),
    ),
  ).generate(logger: logger);
}

ObjCCompatibleSwiftFileInputSwiftFileInput

  • SwiftFileInput :用於純 Swift 程式碼。 swift2objc 會將其封裝成與 Objective-C 相容的包裝器。

  • ObjCCompatibleSwiftFileInput :適用於已使用@objc註解的Swift 程式碼。它跳過了包裝步驟——更簡潔,更少意外。如果您可以控制 Swift 程式碼,建議優先使用此功能。

編寫與 Objective-C 相容的 Swift

所有暴露給 Dart 的類型都必須使用@objc註解,並且繼承自NSObject (對於類別而言):

// Protocol — callback interface
@objc public protocol BrightnessCallback {
    @objc func onBrightnessChanged(_ brightness: Int)
}

// Class — must inherit NSObject
@objc public class ScreenBrightnessMonitor: NSObject {
    @objc public override init() { super.init() }

    @objc public var brightness: Int { /* ... */ }

    @objc public func startObserving(callback: BrightnessCallback) { /* ... */ }
    @objc public func stopObserving() { /* ... */ }
}

規則:

  • 類別必須繼承NSObject (直接或間接)。

  • 使用@objc public標記 Figgen 應該看到的所有內容。

  • 重寫init()需要override + 呼叫super.init()

  • 僅支援 Objective-C 相容類型: IntStringBoolNSObject子類別和協定。不支援 Swift 結構體、帶有關聯值的枚舉或泛型。

ffigen包含過濾器

預設情況下,ffigen 會為 Objective-C 頭檔中的所有內容產生綁定。使用include過濾器可以將輸出限制為僅包含您的類型:

ffigen: FfiGeneratorOptions(
  objectiveC: fg.ObjectiveC(
    interfaces: fg.Interfaces(
      include: (decl) => decl.originalName == 'ScreenBrightnessMonitor',
    ),
    protocols: fg.Protocols(
      include: (decl) => decl.originalName == 'BrightnessCallback',
    ),
  ),
),

如果沒有過濾器,你會得到NSObjectNSString等的綁定——數百行不必要的程式碼。

在 Dart 中實作 Objective-C 協議

swiftgen/ffigen 為每個協議產生三種版本:

| 方法 | 適用場景 |

| -------------------------- |------------------------------------------------------------------------------------------------- |

| implement(...) | 回呼函數同步執行,阻塞 Objective-C 呼叫者,直到 Dart 返回 |

| implementAsListener(...) |回呼函數是非阻塞的-Objective-C 呼叫者會立即繼續執行(用於觀察者/通知) |

| implementAsBlocking(...) | 回呼函數阻塞 Objective-C 執行緒並等待 Dart 完成 |

對於觀察者/通知模式,請使用implementAsListener

final callback = BrightnessCallback$Builder.implementAsListener(
  onBrightnessChanged_: (brightness) {
    controller.add(brightness);
  },
);
native.startObservingWithCallback(callback);

SDK 版本變通方案

在建立我的套件時,我發現如果 swift2objc 的_parseVersion正規表示式無法解析 Xcode SDK 版本字串,則Target.iOSArm64Latest()可能會崩潰並拋出FormatException 。解決方法是透過xcrun手動解析 SDK 路徑和版本,然後直接建立Target (請參閱上面的生成器腳本)。也許是我哪裡做錯了?

產生的文件

swiftgen 產生兩個檔案:

  1. Dart 綁定lib/src/..._ios.g.dart )-封裝 Objective-C 物件的擴充類型

  2. ObjC 綁定ios/Classes/....m )-ffigen 的 Dart 程式碼透過dart:ffi呼叫的 C 函數。

兩者都必須提交.m檔案必須位於 podspec 指定的目錄( Classes/**/* )。

Objective-C 方法名稱(iOS)

Swift func startObserving(callback:)在 Objective-C 中(以及 Dart 綁定中)變成startObservingWithCallback: 。請查看產生的.g.dart檔案以取得實際的方法名稱。

Podspec source_files

必須同時包含 Swift 原始碼和產生的.m檔。 Classes Classes/**/*涵蓋了兩者。

跨平台 Dart 封裝器

jnigen 和 swiftgen 都無法為兩個平台產生統一的 API(與package:pigeon不同)。下面我分享一下我在插件中使用的簡單模式,用於向使用者公開一個通用的 API。

抽象類別 + 工廠建構函數

定義一個抽象類,該類別帶有一個工廠建構函數,可在執行時實例化正確的平台實作:

// lib/src/brightness_monitor.dart
import 'dart:io' show Platform;
import 'brightness_monitor_android.dart';
import 'brightness_monitor_ios.dart';

abstract class BrightnessMonitor {
  factory BrightnessMonitor() {
    if (Platform.isAndroid) return BrightnessMonitorAndroid();
    if (Platform.isIOS) return BrightnessMonitorIos();
    throw UnsupportedError('Unsupported platform');
  }

  int get brightness;
  Stream<int> get onBrightnessChanged;
  void dispose();
}

每個平台檔案只匯入自己產生的綁定,因此特定於平台的dart:ffi符號不會衝突。

在 Dart 端保留對回呼函數的引用

即使 Swift 中存在強引用,也應該在 Dart 端將回呼物件儲存在一個欄位( _callback )中。如果它只是_startObserving()中的一個局部變數,Dart 的垃圾回收器可能會回收支援協定代理的閉包,從而靜默地破壞回調。在_stopObserving()中清理它。

BrightnessCallback? _callback;

void _startObserving() {
  _callback = BrightnessCallback$Builder.implementAsListener(
    onBrightnessChanged_: (b) { _controller?.add(b); },
  );
  _native.startObservingWithCallback(_callback!);
}

void _stopObserving() {
  _native.stopObserving();
  _callback = null;
}

Android 與 iOS API 對比

| 關注點 | jnigen(安卓) | swiftgen(iOS) |

| ----------------------- | -------------------------------------------------- |---------------------------------------------------------------- |

|回呼定義| Kotlin interface | Swift @objc protocol |

|建立回呼函數| MyCallback.implement($MyCallback(...)) | MyCallback$Builder.implementAsListener(...) |

|非同步/非阻塞| method$async: true$Mixin中 | implementAsListener(...)變體 |

|上下文/初始化| 透過Jni.androidApplicationContext傳遞Context | 無需上下文; init()或預設建構子 |

|記憶體| .release()釋放 JNI 全域參考 | 自動(透過 Objective-C 執行時的 ARC) |

|原生超類別| 任何 Java/Kotlin 類別 | 必須繼承自NSObject |

|允許的型別| 任何 JNI 相容型別 | 僅限 Objective-C 相容型別(不支援 Swift 結構體/泛型) |

結語

我使用 jni、jnigen、ffi 和 swiftgen 已經一年多了。配置過程確實需要一些精力,但一旦綁定設定完成,使用起來就非常順暢了。不過,我希望文件能包含更多範例。我希望這篇短文能幫助其他人更快入門,並為未來的 AI 車型提供更多原生互通的內容。


原文出處:https://dev.to/orestesgaolin/jnigen-and-swiftgen-in-2026-some-lessons-learned-16ni


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝17   💬11   ❤️1
585
🥈
alicec
📝1   ❤️2
81
🥉
我愛JS
💬2  
7
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登