JNI 与 Dart FFI 的互操作:在 Android 上绕过 JVM 直接调用 Native 库
大家好!今天我们要深入探讨一个非常有趣且强大的技术领域:JNI(Java Native Interface)和 Dart FFI(Foreign Function Interface)的结合,实现在 Android 平台上绕过 JVM 直接调用 Native 库。
在传统的 Android 开发中,Java 是主要语言,但有时我们需要利用 C/C++ 等 Native 语言的性能优势,例如进行图像处理、音视频编解码、以及访问底层硬件等。JNI 是 Java 虚拟机提供的一套接口,允许 Java 代码调用 Native 代码,反之亦然。然而,JNI 存在一些固有的问题,比如开发过程繁琐、性能损耗以及维护成本高等。
Dart 作为 Flutter 的核心语言,提供了 FFI 机制,允许 Dart 代码直接与 Native 库进行交互,无需通过 JVM。这为我们提供了一种更高效、更灵活的方式来利用 Native 代码。
那么,如何将 JNI 和 Dart FFI 结合起来,在 Android 平台上实现绕过 JVM 直接调用 Native 库呢?这就是我们今天要讨论的核心内容。我们将从 JNI 的基础开始,逐步过渡到 Dart FFI 的使用,并最终展示一个完整的示例,说明如何在 Flutter 应用中通过 FFI 调用 Native 库。
1. JNI 的基础:Java 与 Native 代码的桥梁
JNI 本质上是 Java 与 Native 代码之间的一座桥梁。它定义了一套规范,允许 Java 代码调用 Native 代码,并允许 Native 代码访问 Java 对象。
1.1 JNI 的工作原理
JNI 的工作流程大致如下:
-
定义 Native 方法: 在 Java 类中声明 Native 方法,这些方法没有具体的实现,只是一个声明。
public class MyNativeClass { public native int add(int a, int b); static { System.loadLibrary("mynativelib"); // 加载 Native 库 } } -
生成 Native 方法的头文件: 使用
javah工具(JDK 自带)根据 Java 类生成 C/C++ 头文件。这个头文件包含了 Native 方法的函数签名。javah -jni MyNativeClass生成的头文件
MyNativeClass.h类似如下:/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class MyNativeClass */ #ifndef _Included_MyNativeClass #define _Included_MyNativeClass #ifdef __cplusplus extern "C" { #endif /* * Class: MyNativeClass * Method: add * Signature: (II)I */ JNIEXPORT jint JNICALL Java_MyNativeClass_add (JNIEnv *, jobject, jint, jint); #ifdef __cplusplus } #endif #endif -
实现 Native 方法: 在 C/C++ 文件中实现 Native 方法,并使用 JNI 提供的 API 来访问 Java 对象。
#include "MyNativeClass.h" #include <stdio.h> JNIEXPORT jint JNICALL Java_MyNativeClass_add (JNIEnv *env, jobject obj, jint a, jint b) { printf("Native method add called with a = %d, b = %dn", a, b); return a + b; } -
编译 Native 代码: 将 C/C++ 代码编译成动态链接库(
.so文件)。gcc -shared -fPIC -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -o libmynativelib.so mynative.c -
加载 Native 库: 在 Java 代码中使用
System.loadLibrary()加载 Native 库。 -
调用 Native 方法: 在 Java 代码中调用 Native 方法,JVM 会自动调用对应的 Native 函数。
MyNativeClass myNative = new MyNativeClass(); int result = myNative.add(10, 20); System.out.println("Result: " + result);
1.2 JNI 的优缺点
| 特性 | 优点 | 缺点 |
|---|---|---|
| 性能 | 可以利用 Native 代码的性能优势,例如 C/C++ 在计算密集型任务中的表现。 | JNI 调用本身存在开销,需要在 Java 和 Native 代码之间进行上下文切换,以及数据类型转换。 |
| 功能 | 可以访问底层硬件和操作系统 API,实现 Java 无法实现的功能。 | 开发过程繁琐,需要编写大量的样板代码。 |
| 可移植性 | Native 代码的可移植性取决于所使用的底层 API。 | 维护成本高,需要在 Java 和 Native 代码之间进行同步,并且需要处理内存管理问题。 |
2. Dart FFI:绕过 JVM 直接调用 Native 库
Dart FFI 允许 Dart 代码直接与 Native 库进行交互,无需通过 JVM。这为我们提供了一种更高效、更灵活的方式来利用 Native 代码。
2.1 Dart FFI 的工作原理
Dart FFI 的工作流程大致如下:
-
定义 Native 函数的签名: 使用 Dart FFI 提供的 API 定义 Native 函数的签名。
import 'dart:ffi' as ffi; import 'dart:io' show Platform; // 定义 Native 函数的签名 typedef NativeAddFunc = ffi.Int32 Function(ffi.Int32, ffi.Int32); typedef DartAddFunc = int Function(int, int); -
加载 Native 库: 使用
ffi.DynamicLibrary.open()加载 Native 库。// 加载 Native 库 final String libPath = Platform.isAndroid ? 'libmynativelib.so' // Android 平台 : 'libmynativelib.dylib'; // 其他平台,例如 iOS, macOS final dylib = ffi.DynamicLibrary.open(libPath); -
获取 Native 函数的指针: 使用
dylib.lookupFunction()获取 Native 函数的指针。// 获取 Native 函数的指针 final addFuncPtr = dylib.lookupFunction<NativeAddFunc, DartAddFunc>('add'); -
调用 Native 函数: 使用获取到的函数指针调用 Native 函数。
// 调用 Native 函数 final result = addFuncPtr(10, 20); print('Result: $result');
2.2 Dart FFI 的优缺点
| 特性 | 优点 | 缺点 |
|---|---|---|
| 性能 | 直接调用 Native 代码,避免了 JVM 的开销,性能更高。 | 需要手动管理内存,容易出现内存泄漏和崩溃。 |
| 功能 | 可以访问底层硬件和操作系统 API,实现 Dart 无法实现的功能。 | 对 Native 代码的依赖性更强,需要编写更多的 Native 代码。 |
| 可移植性 | Dart FFI 的可移植性取决于所使用的 Native 代码和平台相关的 API。 | 需要处理不同平台之间的差异,例如 Android 和 iOS 的 Native 库格式不同。 |
| 开发效率 | 相比 JNI,使用 Dart FFI 的开发效率更高,代码更简洁易懂,避免了大量的样板代码。 | 需要熟悉 Dart FFI 的 API 和 Native 代码的开发,学习曲线较陡峭。 |
3. JNI 与 Dart FFI 的结合:绕过 JVM 直接调用 Native 库
现在,让我们来看看如何将 JNI 和 Dart FFI 结合起来,在 Android 平台上实现绕过 JVM 直接调用 Native 库。
3.1 方案概述
我们的目标是:
- 使用 JNI 在 Java 代码中加载 Native 库。
- 在 Native 代码中,通过 Dart FFI 调用另一个 Native 库。
- 在 Flutter 应用中,调用 Java 代码,从而间接调用通过 Dart FFI 调用的 Native 库。
3.2 具体步骤
-
创建 Native 库 (mynativelib.so):
// mynative.c #include <stdio.h> int add(int a, int b) { printf("Native lib called with a = %d, b = %dn", a, b); return a + b; }编译:
gcc -shared -fPIC -o libmynativelib.so mynative.c -
创建另一个 Native 库 (jnilib.so): 这个库将通过 JNI 加载,并使用 Dart FFI 调用
mynativelib.so。// jni_wrapper.c #include <jni.h> #include <stdio.h> #include <dlfcn.h> // For dynamic linking // Define the function signature (matching mynativelib.so) typedef int (*AddFunc)(int, int); JNIEXPORT jint JNICALL Java_com_example_jnidartffi_MainActivity_addFromJNI(JNIEnv *env, jobject thiz, jint a, jint b) { // Load mynativelib.so dynamically void *handle = dlopen("libmynativelib.so", RTLD_LAZY); if (!handle) { printf("Failed to load libmynativelib.so: %sn", dlerror()); return -1; // Or some other error code } // Get the address of the 'add' function AddFunc addFunc = (AddFunc)dlsym(handle, "add"); if (!addFunc) { printf("Failed to find 'add' function: %sn", dlerror()); dlclose(handle); return -1; // Or some other error code } // Call the 'add' function int result = addFunc((int)a, (int)b); // Clean up dlclose(handle); return (jint)result; }编译:
gcc -shared -fPIC -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -ldl -o libjnilib.so jni_wrapper.c注意: 编译时需要链接
libdl.so,因为我们使用了动态链接 (dlopen,dlsym,dlclose)。 -
创建 Android 项目:
- 在
app/src/main/java/com/example/jnidartffi目录下创建MainActivity.java文件。
package com.example.jnidartffi; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends AppCompatActivity { // Declare the native method public native int addFromJNI(int a, int b); // Load the native library static { System.loadLibrary("jnilib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Call the native method int result = addFromJNI(5, 3); // Display the result TextView textView = findViewById(R.id.result_text); textView.setText("Result from JNI: " + result); } }- 在
app/src/main/res/layout目录下创建activity_main.xml文件。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <TextView android:id="@+id/result_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Result will be displayed here" /> </LinearLayout> - 在
-
将 Native 库添加到 Android 项目:
- 在
app/src/main目录下创建jniLibs目录。 - 在
jniLibs目录下创建armeabi-v7a目录 (或其他你想要支持的架构)。 - 将
libjnilib.so和libmynativelib.so复制到app/src/main/jniLibs/armeabi-v7a目录中。
- 在
-
创建 Flutter 应用:
flutter create jni_dart_ffi_app -
在 Flutter 应用中调用 Java 代码:
为了调用 Java 代码,我们需要使用 Flutter 的
MethodChannel。- 修改
lib/main.dart文件:
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'JNI & Dart FFI Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { static const platform = const MethodChannel('com.example.jnidartffi/native'); String _result = 'Press the button to get result'; Future<void> _invokeNativeMethod() async { String result; try { final int nativeResult = await platform.invokeMethod('addFromJNI', {'a': 7, 'b': 5}); result = 'Result from JNI: $nativeResult'; } on PlatformException catch (e) { result = "Failed to invoke native method: '${e.message}'."; } setState(() { _result = result; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('JNI & Dart FFI Demo'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( _result, ), ElevatedButton( onPressed: _invokeNativeMethod, child: Text('Invoke Native Method'), ), ], ), ), ); } } - 修改
-
在 Android 项目中处理 Flutter 的 MethodChannel 调用:
- 修改
MainActivity.java文件:
package com.example.jnidartffi; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.plugin.common.MethodChannel; public class MainActivity extends FlutterActivity { private static final String CHANNEL = "com.example.jnidartffi/native"; // Declare the native method public native int addFromJNI(int a, int b); // Load the native library static { System.loadLibrary("jnilib"); } @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { super.configureFlutterEngine(flutterEngine); new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL) .setMethodCallHandler( (call, result) -> { if (call.method.equals("addFromJNI")) { int a = call.argument("a"); int b = call.argument("b"); int nativeResult = addFromJNI(a, b); result.success(nativeResult); } else { result.notImplemented(); } } ); } } - 修改
-
运行 Flutter 应用:
flutter run
3.3 代码执行流程
- Flutter 应用通过
MethodChannel调用 Android 的MainActivity中的addFromJNI方法。 MainActivity中的addFromJNI方法是一个 Native 方法,它会调用libjnilib.so中的对应函数。libjnilib.so中的函数使用dlopen和dlsym动态加载libmynativelib.so,并调用其中的add函数。libmynativelib.so中的add函数执行加法运算,并将结果返回给libjnilib.so。libjnilib.so将结果返回给MainActivity。MainActivity将结果通过MethodChannel返回给 Flutter 应用。- Flutter 应用显示结果。
4. 示例代码总结
这个例子演示了如何通过 JNI 加载一个 Native 库,然后在该 Native 库中使用 Dart FFI 的思想,动态加载另一个 Native 库并调用其函数。虽然这里没有直接使用 Dart FFI 的 API,但动态加载 Native 库的思想与 Dart FFI 的直接调用 Native 函数类似。
5. 进一步思考
虽然以上示例并没有直接使用 Dart FFI 的 API,但我们可以将这种思想应用到更复杂的场景中。例如,我们可以创建一个 Native 库,该库使用 Dart FFI 调用另一个 Native 库,并将结果返回给 Java 代码。这样,我们就可以在 Android 平台上实现更灵活的 Native 代码调用。
6. 结论:结合现有技术,实现灵活的 Native 调用方案
通过结合 JNI 和动态加载 Native 库的技术,我们可以在 Android 平台上实现一种灵活的 Native 代码调用方案。这种方案可以让我们在 Flutter 应用中利用 Native 代码的性能优势,同时避免 JNI 的一些缺点。 虽然示例展示的是用JNI加载一个native库,然后在该native库中使用动态库加载的思想加载另一个native库,但我们可以借鉴这种思想,构建更复杂的调用链,最终目标是在绕过JVM的前提下,实现Dart FFI风格的 Native 代码调用。