jni 1.0.0版本最近發布了。這是一個很好的機會,可以分享我過去幾年在原生互通方面積累的一些經驗教訓。
JNI 0.14/0.15 和 1.0.0 版本之間存在一些重大變更,因此如果您之前使用過 JNI 和 JNIgen,升級時可能會遇到一些額外的問題。本文的撰寫源自於我在遷移過程中遇到的 AI 代理輸出錯誤問題,這讓我非常惱火。
我發現最快的方法是先註解掉所有原生整合程式碼,然後再重新建置 Android 專案並重新產生綁定。
一些 API 的名稱已經更改(恢復)為更合理的名稱,例如, toDartString()現在變成toString() ,或用於設定屬性的方法變成了setter ( _service.onReplyListener(_listener!);現在是_service.onReplyListener = _listener!; 寫)。還有一些令人煩惱的更改,例如構造函數重寫更改了數字,例如,Intent.new$2現在可能是Intent.new$12` …
此外,新的建議綁定組態定義方式不再是 YAML 文件,而是一個簡單的 Dart 腳本。我非常喜歡這種新方法,因為它隱藏了程式碼生成的底層機制,您只需在生成的程式碼中加入一些預處理和後處理邏輯。看來未來的慣例是將這些腳本放在tool/目錄下,然後使用dart run tool/jnigen.dart或dart run tool/swiftgen.dart來執行它們。
您可以查看簡單 Flutter 應用程式的範例遷移提交:
以下我將分享過去幾年在使用 jnigen 和 swiftgen 時學到的一些經驗教訓。最近幾個月,我更新了一些軟體包:
快速回顧:
您可以使用 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',
],
));
}
如果你想從原生程式碼向 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);
存取 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 目前還不穩定,但我使用至今取得了一些成功。 Swift 程式碼需要與 Objective-C 相容,而 Swiftgen 則透過swift2objc和ffigen處理與 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);
}
ObjCCompatibleSwiftFileInput與SwiftFileInputSwiftFileInput :用於純 Swift 程式碼。 swift2objc 會將其封裝成與 Objective-C 相容的包裝器。
ObjCCompatibleSwiftFileInput :適用於已使用@objc註解的Swift 程式碼。它跳過了包裝步驟——更簡潔,更少意外。如果您可以控制 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 相容類型: Int 、 String 、 Bool 、 NSObject子類別和協定。不支援 Swift 結構體、帶有關聯值的枚舉或泛型。
預設情況下,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',
),
),
),
如果沒有過濾器,你會得到NSObject 、 NSString等的綁定——數百行不必要的程式碼。
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);
在建立我的套件時,我發現如果 swift2objc 的_parseVersion正規表示式無法解析 Xcode SDK 版本字串,則Target.iOSArm64Latest()可能會崩潰並拋出FormatException 。解決方法是透過xcrun手動解析 SDK 路徑和版本,然後直接建立Target (請參閱上面的生成器腳本)。也許是我哪裡做錯了?
swiftgen 產生兩個檔案:
Dart 綁定( lib/src/..._ios.g.dart )-封裝 Objective-C 物件的擴充類型
ObjC 綁定( ios/Classes/....m )-ffigen 的 Dart 程式碼透過dart:ffi呼叫的 C 函數。
兩者都必須提交.m檔案必須位於 podspec 指定的目錄( Classes/**/* )。
Swift func startObserving(callback:)在 Objective-C 中(以及 Dart 綁定中)變成startObservingWithCallback: 。請查看產生的.g.dart檔案以取得實際的方法名稱。
source_files必須同時包含 Swift 原始碼和產生的.m檔。 Classes Classes/**/*涵蓋了兩者。
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符號不會衝突。
即使 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;
}
| 關注點 | 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