JNI 与 Dart FFI 的互操作:在 Android 上绕过 JVM 直接调用 Native 库

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 的工作流程大致如下:

  1. 定义 Native 方法: 在 Java 类中声明 Native 方法,这些方法没有具体的实现,只是一个声明。

    public class MyNativeClass {
        public native int add(int a, int b);
        static {
            System.loadLibrary("mynativelib"); // 加载 Native 库
        }
    }
  2. 生成 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
  3. 实现 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;
    }
  4. 编译 Native 代码: 将 C/C++ 代码编译成动态链接库(.so 文件)。

    gcc -shared -fPIC -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -o libmynativelib.so mynative.c
  5. 加载 Native 库: 在 Java 代码中使用 System.loadLibrary() 加载 Native 库。

  6. 调用 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 的工作流程大致如下:

  1. 定义 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);
  2. 加载 Native 库: 使用 ffi.DynamicLibrary.open() 加载 Native 库。

    // 加载 Native 库
    final String libPath = Platform.isAndroid
        ? 'libmynativelib.so' // Android 平台
        : 'libmynativelib.dylib'; // 其他平台,例如 iOS, macOS
    
    final dylib = ffi.DynamicLibrary.open(libPath);
  3. 获取 Native 函数的指针: 使用 dylib.lookupFunction() 获取 Native 函数的指针。

    // 获取 Native 函数的指针
    final addFuncPtr = dylib.lookupFunction<NativeAddFunc, DartAddFunc>('add');
  4. 调用 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 方案概述

我们的目标是:

  1. 使用 JNI 在 Java 代码中加载 Native 库。
  2. 在 Native 代码中,通过 Dart FFI 调用另一个 Native 库。
  3. 在 Flutter 应用中,调用 Java 代码,从而间接调用通过 Dart FFI 调用的 Native 库。

3.2 具体步骤

  1. 创建 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
  2. 创建另一个 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)。

  3. 创建 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>
  4. 将 Native 库添加到 Android 项目:

    • app/src/main 目录下创建 jniLibs 目录。
    • jniLibs 目录下创建 armeabi-v7a 目录 (或其他你想要支持的架构)。
    • libjnilib.solibmynativelib.so 复制到 app/src/main/jniLibs/armeabi-v7a 目录中。
  5. 创建 Flutter 应用:

    flutter create jni_dart_ffi_app
  6. 在 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'),
                ),
              ],
            ),
          ),
        );
      }
    }
  7. 在 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();
                                }
                            }
                    );
        }
    }
  8. 运行 Flutter 应用:

    flutter run

3.3 代码执行流程

  1. Flutter 应用通过 MethodChannel 调用 Android 的 MainActivity 中的 addFromJNI 方法。
  2. MainActivity 中的 addFromJNI 方法是一个 Native 方法,它会调用 libjnilib.so 中的对应函数。
  3. libjnilib.so 中的函数使用 dlopendlsym 动态加载 libmynativelib.so,并调用其中的 add 函数。
  4. libmynativelib.so 中的 add 函数执行加法运算,并将结果返回给 libjnilib.so
  5. libjnilib.so 将结果返回给 MainActivity
  6. MainActivity 将结果通过 MethodChannel 返回给 Flutter 应用。
  7. 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 代码调用。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注