Flutter JNI交互插件jni的使用

发布于 1周前 作者 ionicwang 来自 Flutter

Flutter JNI交互插件jni的使用

插件简介

package:jni 是一个支持从Dart/Flutter代码访问JNI的库。它为由 jnigen 生成的绑定提供了通用的基础设施,以及一些实用的方法。

该库包含以下内容:

  • 访问JNI中的JNIEnvJavaVM变量的功能,并封装了JNI提供的功能。JNIEnv通过GlobalJniEnv类型暴露出来,提供了一个轻量级的抽象,使其可以在多个线程中使用。
  • 在桌面平台上启动JVM的功能(Jni.spawn)。
  • 一些Android特定的帮助函数(获取应用程序上下文和当前活动引用)。
  • JObject类,作为由jnigen生成的类的基础类。
  • 常用的Java类如JListJMapJInteger等。

除了作为jnigen生成的代码的基础库外,此库还可以用于JNI的一次性使用和调试。要从Java库生成类型安全的绑定,请使用jnigen

文档

test/目录包含了带有注释的文件,解释了本模块的基本用法,而example/目录则包含了一个Flutter示例,也涉及了一些Android特定的内容。

使用这个库假定你对JNI有一定程度的了解——它的线程模型和对象引用等。

迁移到0.8.0版本

有关迁移信息,请参阅changelog

独立使用package:jni(不使用package:jnigen)的API已更改为使用JClass.forName来查找类。通过检索到的JClass访问方法和字段。例如,可以使用jClass.instanceMethodId根据名称和签名查找实例方法。然后可以通过调用结果的JInstanceMethodId来调用实例方法。

示例代码

下面是一个完整的示例demo,展示了如何在Flutter项目中使用package:jni与JNI进行交互。

main.dart

// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// ignore_for_file: library_private_types_in_public_api

import 'dart:ffi';
import 'dart:io';

import 'package:ffi/ffi.dart';
import 'package:flutter/material.dart';
import 'package:jni/jni.dart';

extension on String {
  /// Returns a Utf-8 encoded `Pointer<Char>` with contents same as this string.
  Pointer<Char> toNativeChars(Allocator allocator) {
    return toNativeUtf8(allocator: allocator).cast<Char>();
  }
}

// An example of calling JNI methods using low level primitives.
// GlobalJniEnv is a thin abstraction over JNIEnv in JNI C API.
//
// For a more ergonomic API for common use cases of calling methods and
// accessing fields, see next examples using JObject and JClass.
String toJavaStringUsingEnv(int n) => using((arena) {
      final env = Jni.env;
      final cls = env.FindClass("java/lang/String".toNativeChars(arena));
      final mId = env.GetStaticMethodID(cls, "valueOf".toNativeChars(arena),
          "(I)Ljava/lang/String;".toNativeChars(arena));
      final i = arena<JValue>();
      i.ref.i = n;
      final res = env.CallStaticObjectMethodA(cls, mId, i);
      final str = env.toDartString(res);
      env.DeleteGlobalRef(res);
      env.DeleteGlobalRef(cls);
      return str;
    });

int randomUsingEnv(int n) => using((arena) {
      final env = Jni.env;
      final randomCls = env.FindClass("java/util/Random".toNativeChars(arena));
      final ctor = env.GetMethodID(
          randomCls, "<init>".toNativeChars(arena), "()V".toNativeChars(arena));
      final random = env.NewObject(randomCls, ctor);
      final nextInt = env.GetMethodID(randomCls, "nextInt".toNativeChars(arena),
          "(I)I".toNativeChars(arena));
      final res =
          env.CallIntMethodA(random, nextInt, toJValues([n], allocator: arena));
      env.DeleteGlobalRef(randomCls);
      env.DeleteGlobalRef(random);
      return res;
    });

double randomDouble() {
  final math = JClass.forName("java/lang/Math");
  final random =
      math.staticMethodId("random", "()D").call(math, jdouble.type, []);
  math.release();
  return random;
}

int uptime() {
  return JClass.forName("android/os/SystemClock").use(
    (systemClock) => systemClock
        .staticMethodId("uptimeMillis", "()J")
        .call(systemClock, jlong.type, []),
  );
}

String backAndForth() {
  final jstring = '🪓'.toJString();
  final dartString = jstring.toDartString(releaseOriginal: true);
  return dartString;
}

void quit() {
  JObject.fromReference(Jni.getCurrentActivity()).use((ac) =>
      ac.jClass.instanceMethodId("finish", "()V").call(ac, jvoid.type, []));
}

void showToast(String text) {
  // This is example for calling your app's custom java code.
  // Place the Toaster class in the app's android/ source Folder, with a Keep
  // annotation or appropriate proguard rules to retain classes in release mode.
  //
  // In this example, Toaster class wraps android.widget.Toast so that it
  // can be called from any thread. See
  // android/app/src/main/java/com/github/dart_lang/jni_example/Toaster.java
  final toasterClass =
      JClass.forName('com/github/dart_lang/jni_example/Toaster');
  final makeText = toasterClass.staticMethodId(
      'makeText',
      '(Landroid/app/Activity;Landroid/content/Context;'
          'Ljava/lang/CharSequence;I)'
          'Lcom/github/dart_lang/jni_example/Toaster;');
  final toaster = makeText.call(toasterClass, JObject.type, [
    Jni.getCurrentActivity(),
    Jni.getCachedApplicationContext(),
    '😀'.toJString(),
    0,
  ]);
  final show = toasterClass.instanceMethodId('show', '()V');
  show(toaster, jvoid.type, []);
}

void main() {
  if (!Platform.isAndroid) {
    Jni.spawn();
  }
  final examples = [
    Example("String.valueOf(1332)", () => toJavaStringUsingEnv(1332)),
    Example("Generate random number", () => randomUsingEnv(180),
        runInitially: false),
    Example("Math.random()", () => randomDouble(), runInitially: false),
    if (Platform.isAndroid) ...[
      Example("Minutes of usage since reboot",
          () => (uptime() / (60 * 1000)).floor()),
      Example("Back and forth string conversion", () => backAndForth()),
      Example("Device name", () {
        final buildClass = JClass.forName("android/os/Build");
        return buildClass
            .staticFieldId("DEVICE", JString.type.signature)
            .get(buildClass, JString.type)
            .toDartString(releaseOriginal: true);
      }),
      Example(
        "Package name",
        () => JObject.fromReference(Jni.getCurrentActivity()).use((activity) =>
            activity.jClass
                .instanceMethodId("getPackageName", "()Ljava/lang/String;")
                .call(activity, JString.type, [])),
      ),
      Example("Show toast", () => showToast("Hello from JNI!"),
          runInitially: false),
      Example(
        "Quit",
        quit,
        runInitially: false,
      ),
    ]
  ];
  runApp(MyApp(examples));
}

class Example {
  String title;
  dynamic Function() callback;
  bool runInitially;
  Example(this.title, this.callback, {this.runInitially = true});
}

class MyApp extends StatefulWidget {
  const MyApp(this.examples, {Key? key}) : super(key: key);
  final List<Example> examples;

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('JNI Examples'),
        ),
        body: ListView.builder(
            itemCount: widget.examples.length,
            itemBuilder: (context, i) {
              final eg = widget.examples[i];
              return ExampleCard(eg);
            }),
      ),
    );
  }
}

class ExampleCard extends StatefulWidget {
  const ExampleCard(this.example, {Key? key}) : super(key: key);
  final Example example;

  @override
  _ExampleCardState createState() => _ExampleCardState();
}

class _ExampleCardState extends State<ExampleCard> {
  Widget _pad(Widget w, double h, double v) {
    return Padding(
        padding: EdgeInsets.symmetric(horizontal: h, vertical: v), child: w);
  }

  bool _run = false;

  @override
  void initState() {
    super.initState();
    _run = widget.example.runInitially;
  }

  @override
  Widget build(BuildContext context) {
    final eg = widget.example;
    var result = "";
    var hasError = false;
    if (_run) {
      try {
        result = eg.callback().toString();
      } on Exception catch (e) {
        hasError = true;
        result = e.toString();
      } on Error catch (e) {
        hasError = true;
        result = e.toString();
      }
    }
    var resultStyle = const TextStyle(fontFamily: "Monospace");
    if (hasError) {
      resultStyle = const TextStyle(fontFamily: "Monospace", color: Colors.red);
    }
    return Card(
      child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
        Text(eg.title,
            softWrap: true,
            style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
        _pad(
            Text(result.toString(), softWrap: true, style: resultStyle), 8, 16),
        _pad(
            ElevatedButton(
              child: Text(_run ? "Run again" : "Run"),
              onPressed: () => setState(() => _run = true),
            ),
            8,
            8),
      ]),
    );
  }
}

说明

  1. 初始化JVM:如果平台不是Android,则调用Jni.spawn()启动JVM。
  2. 定义示例功能:创建了一系列示例功能,包括字符串转换、随机数生成、设备信息获取、Toast显示等。
  3. UI展示:通过Flutter的ListView.builder展示每个示例功能的结果,并允许用户点击按钮运行或重新运行示例。

注意事项

  • 线程模型:确保理解JNI的线程模型,特别是在多线程环境中使用时。
  • 对象引用:正确管理JNI对象引用,以避免内存泄漏。
  • 自定义Java代码:如果需要调用自定义的Java代码(如Toaster类),请确保在Android项目的源码中正确放置这些类,并添加适当的ProGuard规则以保留这些类。

希望这个示例能帮助你更好地理解和使用Flutter中的JNI交互插件。如果有任何问题或需要进一步的帮助,请随时提问!


更多关于Flutter JNI交互插件jni的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter JNI交互插件jni的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在处理Flutter与原生Android代码之间的交互时,JNI(Java Native Interface)是一种常用的技术。以下是一个简要的示例,展示如何在Flutter插件中使用JNI进行交互。

1. 设置Flutter插件项目

首先,确保你已经创建了一个Flutter插件项目。如果还没有,可以使用以下命令创建一个新的Flutter插件项目:

flutter create --template=plugin my_flutter_plugin
cd my_flutter_plugin

2. 在Android原生代码中定义Native方法

android/src/main/cpp/目录下创建一个C++文件(例如,native-lib.cpp),并定义你的Native方法:

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_my_flutter_plugin_MyFlutterPlugin_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

3. 在Java代码中加载Native库并声明Native方法

android/src/main/java/com/example/my_flutter_plugin/MyFlutterPlugin.java中,加载Native库并声明Native方法:

package com.example.my_flutter_plugin;

import android.content.Context;

import androidx.annotation.NonNull;

import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;

public class MyFlutterPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
  private MethodChannel channel;
  private Context applicationContext;

  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "my_flutter_plugin");
    channel.setMethodCallHandler(this);
    applicationContext = flutterPluginBinding.getApplicationContext();

    // Load native library
    System.loadLibrary("native-lib");
  }

  // This static block is not strictly necessary, but it can help identify
  // when your native code is being loaded.
  static {
    System.loadLibrary("native-lib");
  }

  // Declare the native method corresponding to the one in native-lib.cpp
  public native String stringFromJNI();

  @Override
  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
    if (call.method.equals("getPlatformVersion")) {
      String version = stringFromJNI();
      result.success("Android " + version); // This is just an example; we're actually returning the JNI result
    } else {
      result.notImplemented();
    }
  }

  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
    channel.setMethodCallHandler(null);
  }

  @Override
  public void onAttachedToActivity(ActivityPluginBinding binding) {
    // No-op
  }

  @Override
  public void onDetachedFromActivityForConfigChanges() {
    // No-op
  }

  @Override
  public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {
    // No-op
  }

  @Override
  public void onDetachedFromActivity() {
    // No-op
  }
}

注意:上面的代码中,stringFromJNI方法在Java中被声明为native,并在native-lib.cpp中实现。但在实际调用中,为了简化示例,我们并没有直接从JNI调用获取字符串,而是返回了一个硬编码的字符串。在实际应用中,你可能需要根据JNI调用的结果来构建返回的字符串。

4. 在Flutter代码中调用插件方法

lib/my_flutter_plugin.dart中,定义你的Flutter插件接口并调用原生方法:

import 'package:flutter/services.dart';

class MyFlutterPlugin {
  static const MethodChannel _channel = MethodChannel('my_flutter_plugin');

  static Future<String?> get platformVersion async {
    final String? version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

然后,在你的Flutter应用中调用这个方法:

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter JNI Demo'),
        ),
        body: Center(
          child: FutureBuilder<String?>(
            future: MyFlutterPlugin.platformVersion,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.done) {
                if (snapshot.hasError) {
                  return Text('Error: ${snapshot.error!}');
                }
                return Text('Result: ${snapshot.data!}');
              } else {
                return CircularProgressIndicator();
              }
            },
          ),
        ),
      ),
    );
  }
}

5. 构建和运行项目

确保你的CMakeLists.txtbuild.gradle文件配置正确以支持C++编译。然后,你可以构建并运行你的Flutter项目:

flutter pub get
cd android
./gradlew assembleDebug
cd ..
flutter run

这个示例展示了如何在Flutter插件中使用JNI进行基本的交互。在实际应用中,你可能需要根据具体需求调整和扩展这个示例。

回到顶部