Flutter JNI集成插件jnigen的使用
Flutter JNI集成插件jnigen的使用
Introduction
jnigen
是一个实验性的绑定生成器,用于通过 dart:ffi
和 JNI 为 Java 绑定生成 Dart 代码。它扫描编译后的 JAR 文件或 Java 源代码以生成 API 描述,然后利用该描述生成 Dart 绑定。这些 Dart 绑定调用 C 绑定,进而通过 JNI 调用 Java 函数。共享功能和基础类由支持库 package:jni
提供。
配置绑定生成通常通过 YAML 文件提供。要生成绑定,需要三个基本配置:
- Inputs:输入可以是 Java 源文件(
source_path
),或编译后的类/JAR 文件(class_path
)。还提供了基于 Maven/Gradle 的工具来简化依赖项获取。 - Outputs:输出可以是包结构化(每个类一个文件)或单个文件绑定。需要指定写入 Dart 绑定的目标路径。
- Classes:指定需要绑定的类或包。指定包时会递归包含所有类。
更多示例配置请参阅 examples。
Example
Java 文件
这是一个简单的 Java 文件,位于 Flutter Android 应用中:
package com.example.in_app_java;
import android.app.Activity;
import android.widget.Toast;
import androidx.annotation.Keep;
@Keep
public abstract class AndroidUtils {
// Hide constructor
private AndroidUtils() {}
public static void showToast(Activity mainActivity, CharSequence text, int duration) {
mainActivity.runOnUiThread(() -> Toast.makeText(mainActivity, text, duration).show());
}
}
Dart Bindings
以下是生成的 Dart 绑定代码:
/// Some boilerplate is omitted for clarity.
class AndroidUtils extends jni.JObject {
@override
late final jni.JObjType<AndroidUtils> $type = type;
AndroidUtils.fromReference(
jni.JReference reference,
) : super.fromReference(reference);
static final _class =
jni.JClass.forName(r"com/example/in_app_java/AndroidUtils");
/// The type which includes information such as the signature of this class.
static const type = $AndroidUtilsType();
static final _id_showToast = _class.staticMethodId(
r"showToast",
r"(Landroid/app/Activity;Ljava/lang/CharSequence;I)V",
);
static final _showToast = ProtectedJniExtensions.lookup<
ffi.NativeFunction<
jni.JThrowablePtr Function(
ffi.Pointer<ffi.Void>,
jni.JMethodIDPtr,
ffi.VarArgs<
(
ffi.Pointer<ffi.Void>,
ffi.Pointer<ffi.Void>,
ffi.Int64
)>)>>("globalEnv_CallStaticVoidMethod")
.asFunction<
jni.JThrowablePtr Function(ffi.Pointer<ffi.Void>, jni.JMethodIDPtr,
ffi.Pointer<ffi.Void>, ffi.Pointer<ffi.Void>, int)>();
/// from: `static public void showToast(android.app.Activity mainActivity, java.lang.CharSequence text, int duration)`
static void showToast(
jni.JObject mainActivity,
jni.JObject text,
int duration,
) {
_showToast(_class.reference.pointer, _id_showToast as jni.JMethodIDPtr,
mainActivity.reference.pointer, text.reference.pointer, duration)
.check();
}
}
YAML 配置
用于生成上述代码的 YAML 配置如下:
android_sdk_config:
add_gradle_deps: true
output:
dart:
path: lib/android_utils.dart
structure: single_file
source_path:
- 'android/app/src/main/java'
classes:
- 'com.example.in_app_java.AndroidUtils'
完整的示例可以在 jnigen/example/in_app_java 中找到,其中添加了几个额外的类来演示使用来自 Gradle JAR 和源依赖项的类。
Supported Platforms
Platform | Dart Standalone | Flutter |
---|---|---|
Android | n/a | Supported |
Linux | Supported | Supported |
Windows | Supported | Supported |
MacOS | Supported | Not Yet |
在 Android 上,Flutter 应用程序嵌入在 Android JVM 中运行。在其他平台上,需要显式地使用 Jni.spawn
启动 JVM。package:jni
提供了初始化和管理 JNI 的基础设施,适用于 Android 和非 Android 平台。
Java Features Support
目前,jnigen
支持 Java 语言的基本特性。每个 Java 类映射到一个 Dart 类。为方法、构造函数和字段生成绑定。Java 中抛出的异常会在 Dart 中重新抛出,并带有来自 Java 的堆栈跟踪。
更高级的功能(如回调)尚未支持。有关这些特性的支持问题,请参阅 issue tracker。
Requirements
SDK
需要 Flutter SDK。
对于 Dart 独立目标,由于 pubspec 格式的一些问题,dart
命令必须来自 Flutter SDK 而不是 Dart SDK。详见 dart-lang/pub#3563。
Java Tooling
使用 JDK 版本 11 到 17。更新版本由于与 Gradle 的兼容性问题将无法工作。
除了 JDK,还需要 maven (mvn
命令)。在 Windows 上,可以通过包管理器如 chocolatey 或 scoop 安装。
在 Windows 上,将 JDK 安装目录中的 jvm.dll
路径添加到 PATH。
例如,在 PowerShell 中:
$env:Path += ";${env:JAVA_HOME}\bin\server".
如果未设置 JAVA_HOME,找到 java.exe
可执行文件并在控制面板中设置环境变量。如果通过包管理器安装了 Java,可能有更自动的方法来完成此操作(例如 scoop reset
)。
C Tooling
需要 CMake 和标准 C 工具链来构建 package:jni
。
FAQs
我在运行时遇到 ClassNotFoundError。
jnigen
不处理将类引入应用程序。这需要通过特定于目标的机制来完成。例如,在 Android 上添加 Gradle 依赖项,或在桌面/独立目标上手动提供 Jni.spawn
的类路径。
在 Android 上,proguard
会剪裁它认为不可访问的类。由于 JNI 类查找发生在运行时,这会导致发布模式下的 ClassNotFound 错误,即使依赖项已包含在 Gradle 中。in_app_java 示例 讨论了两种防止这种情况的方法:使用 Keep
注解 (androidx.annotation.Keep
) 以及 proguard-rules 文件。
最后,某些库(如 java.awt
)在 Android 中不存在。尝试使用依赖于它们的库也会导致 ClassNotFound 错误。
jnigen
找不到类。
确保您提供了正确的源和类路径,并且它们遵循标准的目录结构。如果您的类名为 com.abc.MyClass
,则 MyClass
必须位于相对于其中一个源路径的 com/abc/MyClass.java
或相对于其中一个类路径的 com/abc/MyClass.class
。
如果类在 JAR 文件中,请确保提供 JAR 文件本身的路径,而不是包含它的目录。
jnigen
无法解析源代码。
如果错误类似于 symbol not found
,请确保所有源依赖项都可用。如果依赖项已编译,它可以包含在 class_path
中。
类是如何映射到绑定中的?
每个 Java 类生成一个 JObject
类的子类,该子类包装了一个 JNI jobject
引用。嵌套类使用 _
作为分隔符,Example.NestedClass
将映射到 Example_NestedClass
。
JObject
持有一个本地引用还是全局引用?是否需要手动释放?
每个返回到 Dart 的 Java 对象都会创建一个 JNI 全局引用。引用删除由 NativeFinalizer
处理,这通常就足够了。
最好保持语言之间的接口简洁。然而,如果需要创建多个引用(例如在一个循环中),您可以使用 FFI Arena 机制 (using
函数) 和 releasedBy
方法,或手动释放对象使用 release
方法。
是否应该使用 jnigen
而不是 Method channels?
这是一个实验性的包。许多特性缺失,且有些粗糙。欢迎您试用并提供反馈。
YAML Configuration Reference
以下是 YAML 配置的参考:
配置属性 | 类型 / 值 | 描述 |
---|---|---|
preamble |
文本 | 生成文件开头粘贴的文本 |
source_path |
目录路径列表 | 搜索源文件的目录 |
class_path |
目录/JAR路径列表 | API 摘要生成的类路径 |
classes * |
合格类/包名列表 | 合格类/包名列表 |
enable_experiment |
实验名称列表 | 启用的实验列表 |
output: |
子部分 | 输出文件相关配置 |
output: >> dart: |
子部分 | Dart 输出配置 |
output: >> dart: >> structure |
package_structure / single_file |
结果 Dart 绑定是否映射到每个类一个文件的源布局,或写入单个文件 |
output: >> dart: >> path * |
目录路径或文件路径 | 写入 Dart 绑定的路径 |
non_null_annotations: |
自定义注解完全限定名列表 | 指定注解类型为非空 |
nullable_annotations: |
自定义注解完全限定名列表 | 指定注解类型为空 |
maven_downloads: |
子部分 | 自动下载 Java 依赖项(源和 JAR)的配置 |
maven_downloads: >> source_deps |
Maven 包坐标列表 | 使用 Maven 下载并解压的源包 |
maven_downloads: >> source_dir |
路径 | 解压缩 Maven 源的目录 |
maven_downloads: >> jar_only_deps |
Maven 包坐标列表 | 非强制性传递依赖的 JAR 依赖 |
maven_downloads: >> jar_dir |
路径 | 存储下载 JAR 的目录 |
log_level |
日志级别 | 配置日志级别,默认为 info |
android_sdk_config: |
子部分 | Android 依赖和 SDK 的自动检测配置 |
android_sdk_config: >> add_gradle_deps |
布尔值 | 是否添加 Gradle 依赖 |
android_sdk_config: >> android_example |
目录路径 | 插件项目的相对路径 |
summarizer: |
子部分 | 构建 API 描述的配置 |
summarizer: >> backend |
auto , doclet 或 asm |
指定 API 摘要生成使用的后端 |
exclude: |
子部分 | 使用正则过滤器排除方法或字段 |
exclude: >> methods |
方法列表 | 排除的方法 |
exclude: >> fields |
字段列表 | 排除的字段 |
Android Core Libraries
现代 Android 项目严重依赖通过 Gradle 下载的 AndroidX 和其他库。我们有一个问题追踪来改进 Android SDK 和依赖项的检测 (#31)。目前,我们可以通过运行 Gradle stub 来获取 Android 项目的 JAR 依赖项,如果指定了 android_sdk_config
>> add_gradle_deps
。
但是核心库(android.**
命名空间)不是通过 Gradle 下载的。核心库随 Android SDK 发布为存根 JAR 文件($SDK_ROOT/platforms/android-$VERSION/android-stubs-src.jar
)。
目前我们没有自动使用这些库的机制。您可以手动解压此 JAR 文件到某个目录并将其作为源路径提供。
但是有两个注意事项:
- SDK 存根在版本 28 之后不完整。我们使用的 OpenJDK Doclet API 在处理不完整的源代码时会报错。
- API 无法处理 Android SDK 存根中的
java.**
命名空间,因为它期望模块布局。因此,如果您想为java.lang.Math
生成绑定,则不能使用 Android SDK 存根。可以使用 OpenJDK 源代码。
相反,可以使用 JAR 文件($SDK_ROOT/platforms/android-$VERSION/android.jar
)。但编译后的 JAR 文件不包括 JavaDoc 和方法参数名称。当指定 android_sdk_config
>> add_gradle_deps
时,此 JAR 文件会自动由 Gradle 包含。
Migrating to 0.8.0
请查看 jni 的 changelog 和 jnigen 的 changelog。
Contributing
请参阅 wiki 获取架构相关文档。
Examples
创建基于 jnigen 的插件
Dart Package (Standalone only)
- 创建 Dart 包,添加
jni
作为依赖项,jnigen
作为开发依赖项。 - 编写类似于 pdfbox_plugin 的 jnigen 配置。
- 通过运行
dart run jnigen --config jnigen.yaml
生成 JNI 绑定。 - 在 CLI 项目中添加此包和
jni
作为依赖项。 - 运行
dart run jni:setup
以构建 JNI 基础库和 jnigen 生成的包的原生库。 - 导入包。参见 pdf_info.dart 了解如何从 Dart 独立环境中使用 JNI。
Flutter FFI Plugin
Flutter FFI 插件的优势在于可以将所需的原生库打包到 Android/Linux 桌面应用中。
要创建带有 JNI 绑定的 FFI 插件:
- 使用
plugin_ffi
模板创建插件。 - 删除 ffigen 特定文件和存根。
- 按照上述步骤生成 JNI 绑定。插件可以从 Flutter 项目中使用。
- 可能希望将绑定生成到私有目录(例如
lib/src/third_party
)并在顶级 Dart 文件中重新导出类。 - 如果希望从 Dart 项目中使用插件,请注释掉或移除 pubspec 中的 Flutter SDK 要求。但这在发布包时是有问题的。
Android Plugin with Custom Java Code
- 创建仅支持 Android 平台的 FFI 插件。
- 使用命令
flutter build apk
构建 example/ Android 项目。完成发布构建后,jnigen 可以使用 Gradle 存根收集编译类路径。 - 在插件的
android/src/main/java
层次结构中编写自定义 Java 代码。 - 按照上述步骤生成 JNI 绑定。参见 notification_plugin/jnigen.yaml 的示例配置。
Pure Dart Bindings
使用纯 Dart 绑定 PoC,大多数 FFI 设置步骤都不是必需的。例如,可以创建一个简单的 Flutter 包而不是 FFI 插件,因为没有原生工件需要打包。
生成的绑定仍然依赖于 package:jni
,因此在独立目标上运行 dart run jni:setup
仍然是必需的。
以上内容涵盖了 jnigen
插件的主要功能和使用方法。希望这对您有所帮助!如果有任何问题或需要进一步的帮助,请随时提问。
更多关于Flutter JNI集成插件jnigen的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter JNI集成插件jnigen的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
在Flutter项目中,使用jnigen
工具可以简化JNI(Java Native Interface)集成插件的开发过程。jnigen
是一个用于生成JNI绑定代码的工具,它可以根据C/C++头文件自动生成JNI相关的Java类和C/C++存根代码。以下是一个使用jnigen
集成Flutter插件的示例代码案例。
1. 设置Flutter项目
首先,确保你已经创建了一个Flutter项目。如果还没有,可以使用以下命令创建一个新的Flutter项目:
flutter create my_flutter_app
cd my_flutter_app
2. 添加jnigen
依赖
在你的Flutter项目的根目录下,找到pubspec.yaml
文件,并添加jnigen
依赖:
dependencies:
flutter:
sdk: flutter
jnigen: ^x.y.z # 替换为最新版本号
然后运行flutter pub get
来安装依赖。
3. 配置CMakeLists.txt
在Flutter项目的ios/
和android/
目录下,你可能需要配置CMakeLists.txt文件来编译你的C/C++代码。以下是一个简单的CMakeLists.txt示例:
cmake_minimum_required(VERSION 3.10)
project(MyNativePlugin)
set(CMAKE_CXX_STANDARD 11)
add_library(
my_native_lib
SHARED
my_native_code.cpp # 替换为你的C/C++源文件
)
find_library(
log-lib
log
)
target_link_libraries(
my_native_lib
${log-lib}
)
4. 创建C/C++头文件和实现文件
在android/app/src/main/cpp/
目录下(如果没有这个目录,请创建它),创建一个C/C++头文件和实现文件。例如,my_native_code.h
和my_native_code.cpp
。
my_native_code.h
#ifndef MY_NATIVE_CODE_H
#define MY_NATIVE_CODE_H
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jint JNICALL
Java_com_example_myflutterapp_MyNativePlugin_nativeAdd(JNIEnv* env, jobject /* this */, jint a, jint b);
#ifdef __cplusplus
}
#endif
#endif //MY_NATIVE_CODE_H
my_native_code.cpp
#include "my_native_code.h"
JNIEXPORT jint JNICALL
Java_com_example_myflutterapp_MyNativePlugin_nativeAdd(JNIEnv* env, jobject /* this */, jint a, jint b) {
return a + b;
}
5. 使用jnigen
生成JNI绑定代码
在Flutter项目的根目录下,创建一个jnigen
配置文件,例如jnigen.yaml
:
output: ios/Runner/jni_gen/ # iOS JNI生成目录
output: android/app/src/main/jniLibs/ # Android JNI生成目录
headers:
- android/app/src/main/cpp/my_native_code.h
classes:
- name: MyNativePlugin
methods:
- name: nativeAdd
return: int
params:
- type: int
name: a
- type: int
name: b
然后运行flutter pub run jnigen
来生成JNI绑定代码。
6. 在Flutter中调用本地方法
在Flutter项目的Dart代码中,你可以通过MethodChannel
来调用本地方法。首先,在lib/
目录下创建一个新的Dart文件,例如my_native_plugin.dart
:
import 'package:flutter/services.dart';
class MyNativePlugin {
static const MethodChannel _channel = MethodChannel('com.example.myflutterapp/my_native_plugin');
static Future<int> nativeAdd(int a, int b) async {
final int result = await _channel.invokeMethod('nativeAdd', {'a': a, 'b': b});
return result;
}
}
然后,在你的主Dart文件(例如lib/main.dart
)中使用这个插件:
import 'package:flutter/material.dart';
import 'my_native_plugin.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter JNI Example'),
),
body: Center(
child: ElevatedButton(
onPressed: () async {
final int result = await MyNativePlugin.nativeAdd(5, 3);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Result: $result")));
},
child: Text('Add Numbers'),
),
),
),
);
}
}
7. 配置Android和iOS原生项目
确保在android/app/build.gradle
中配置了CMake和NDK:
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
}
对于iOS,确保在ios/Runner/Info.plist
中添加了必要的权限,并在ios/Runner/AppDelegate.swift
中配置FlutterEngine
。
8. 运行Flutter应用
最后,运行你的Flutter应用:
flutter run
现在,你应该能够在Flutter应用中调用本地C/C++代码了。这个示例展示了如何使用jnigen
工具来简化JNI集成插件的开发过程。