通过本篇文章,你应该达成以下目标:
- 了解什么是JNI,了解JNI与Java的关系,了解JNI与JVM的关系。
- 了解Java如何调用JNI,如何通过c/c++实现Java的native方法。
- 了解如何在JNI内调用Java的方法。
macos c/c++工程构建参考:macos+vscode+cmake构建c/c++工程
1 基础
1.1 什么是JNI
JNI是Java Native Interface的缩写,通过使用Java本地接口书写程序,可以确保代码在不同的平台上方便移植。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。
本篇文章主要介绍Java如何通过JNI与C或者C++进行交互。
1.2 JNI数据类型
JNI在Java与C/C++之间起中转作用,考虑到Java与C/C++语言之间的数据类型不统一,JNI有一套自己的数据类型,分别与Java和C/C++进行对应,是一套中间数据类型。
Java | JNI | 描述 |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | signed 32 bits |
double | jdouble | signed 64 bits |
Class | jclass | class类对象 |
String | jstring | 字符串对象 |
Object | jobject | 任何Java对象 |
byte[] | jbyteArray | byte数组 |
1.3 JNI上下文
JNIEnv类型实际上代表了Java环境,通过JNIEnv*指针,JNI函数可以对Java侧的代码进行操作。例如,创建Java类的对象,调用Java对象的方法,获取对象中的属性等。JNIEnv的指针会被传入到JNI侧的native方法的实现函数中,来对Java端的代码进行操作。例如:
jstring f1(JNIEnv *env, jclass jobj){
return (*env)->NewStringUTF(env,"Hi Java");
}
复制代码
区分jobject与jclass:在JNI侧声明Java native方法的实现的时候,会有两个默认形参(除开native方法自己的传入参数),分别是JNIEnv指针,另外一个是jobject/jclass,这两个的区别在于: jobject:如果Java侧的native方法是非静态的,那么传给JNI的第二个参数是类的对象,所有类的对象在JNI侧都是jobject类型。jclass:如果Java侧的native方法是静态的,那么传给JNI的第二个参数是类的运行时类,所有运行时类在JNI侧都是jclass类型。
JNIEXPORT和JNICALL:JNIEXPORT和JNICALL是两个关键字,表明这个方法是JNI方法。
JNIEXPORT jint JNICALL Java_register_staticRegister_StaticRegister_func
(JNIEnv *env, jclass jobj,jintArray arr){
int len = (*env)->GetArrayLength(env,arr);
return len;
}
复制代码
1.4 Java签名
查看Java字段和方法的签名
javap命令:javap -s -p JniTest.class
Java签名规范
在JNI标准中,Java方法签名有以下约定。
数据类型 | 签名 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
object | L开头,然后以‘/’分隔包的完整类型,最后加上‘;’(分号)。 比如: String签名是Ljava/lang/String; |
Array | [开头,后面加上元素类型的签名。 比如: int[]的签名是[I;int[][]的签名是[[I; object[]签名是[Ljava/lang/Object;。 |
举例
//Java:
import java.util.Data;
public class Hello {
public int property;
public int function(int fu,Date date,int[] arr) {
return 0;
}
public native void test();
}
复制代码
//C++:
JNIEXPORT void JNICALL Java_Hello_test(JNIEnv *env, jobject jobj){
jclass hello_clz = (*env) -> GetObjectClass(env, jobj);
jfieldID fieldID_prop = (*env) -> GetFieldID(env, hello_claz, "property", "I");
jmethodID methodID_func =
(*env) -> GetMethodID(env, hello_clz, "function", "(ILjava/util/Date;[I)I");
(*env) -> CallIntMethod(env, jobj, methodID_func, 0L, NULL, NULL);
}
复制代码
2 Java调用JNI
Java定义native方法后,JNI具体实现native方法,将Java定义与JNI实现进行关联后,Java直接调用native方法即可使用到JNI的实现。上述的关联操作也被称为注册,换句话说,注册是指将JNI的函数与Java native方法关联在一起的必要操作,而注册分为静态注册和动态注册。
2.1 静态注册
步骤:
- 编写Java类,比如Hello.java;
package learn.java.jni;
public class Hello {
public static native String jniSaysHello();
public static String javaSaysHello() {
return "Java: Hello JNI, this is Java!";
}
public static void main(String[] args) {
System.out.println(javaSaysHello());
System.out.println(jniSaysHello());
}
}
复制代码
- 在.java源文件目录下,命令行输入“javac Hello.java”生成Hello.class文件;
- 在Hello.class所属包所在目录下,命令行执行“javah learn.java.Hello”(完整类名无后缀),在包所在目录生成learn_java_jni_Hello.h头文件;
- 如果是JDK 1.8或以上,以上步骤可简化为一步:在Hello.java目录下,命令行执行 javac -h . Hello.java,直接在当前目录下得到.class文件和.h文件;
- 创建C/C++项目并拷贝learn_java_jni_Hello.h文件到项目目录;
- 在C/C++项目中添加jni.h头文件和jni_md.h头文件,这两个头文件是JDK自带的,在JAVA_HOME/include目录下,将这两个头文件拷贝到c/c++项目目录;
- 在learn_java_jni_Hello.h中修改#include <jni.h>为#include "jni.h";
- 可以看到,learn_java_jni_Hello.h文件里面就是一个Java方面native方法的一个JNI声明,格式为JNIEXPORT 关键字一 jstring 返回值的JNI类型 JNICALL 关键字二 Java_learn_java_jni_Hello_jniSaysHello Java_全类名_方法名 (JNIEnv *, jclass);如下;
/* DO NOT EDIT THIS FILE - it is machine generated */
#include "jni.h"
/* Header for class learn_java_jni_Hello */
#ifndef _Included_learn_java_jni_Hello
#define _Included_learn_java_jni_Hello
#ifdef __cplusplus
extern "C"
{
#endif
/*
* Class: learn_java_jni_Hello
* Method: jniSaysHello
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_learn_java_jni_Hello_jniSaysHello(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
复制代码
- 编写头文件learn_java_jni_Hello.h对应的learn_java_jni_Hello.c源文件,拷贝并实现learn_java_jni_Hello.h下的函数,如下:
#include "learn_java_jni_Hello.h"
JNIEXPORT jstring JNICALL Java_learn_java_jni_Hello_jniSaysHello(JNIEnv *env, jclass jc)
{
return (*env)->NewStringUTF(env, "JNI: Hello Java, this is JNI!");
}
复制代码
- 编写CMakeLists.txt文件,这是库的配置文件。最重要的是最后两个add_library(),其余的都是自动生成的。add_library()声明库的名字、类型和包含的.c&.h文件。SHARED关键字表示创建的库是动态库.dll(windows)/.dylib(mac),STATIC关键字表示创建的库是静态库.a(windows&mac)。注意:库本身有动态库和静态库之分,Java native方法也有静态注册和动态注册之分,二者没有关系。 这里将动态库命名为JniSaysHello;
cmake_minimum_required(VERSION 3.0.0)
project(jnicppdemo VERSION 0.1.0)
include(CTest)
enable_testing()
add_library(JniSaysHello SHARED learn_java_jni_Hello.c learn_java_jni_Hello.h)
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)
复制代码
- 此时C/C++项目结构如下图,Build Project生成动态链接库,得到libJniSaysHello.dylib;
- 在Java侧,Hello.Java源文件中,添加静态代码块,使用System.load()方法加载该动态链接库,如下:
package learn.java.jni;
public class Hello {
static {
System.load("/Users/tongbo/Documents/Projects/jni/jnic/jnicppdemo/build/libJniSaysHello.dylib");
}
public static native String jniSaysHello();
public static String javaSaysHello() {
return "Java: Hello JNI, this is Java!";
}
public static void main(String[] args) {
System.out.println(javaSaysHello());
System.out.println(jniSaysHello());
}
}
复制代码
- 在Java侧运行,得到如下效果,Java成功调用了dylib中的方法,静态注册成功。
- 上述过程,JNI使用Java_PACKAGENAME_CLASSNAME_METHODNAME与Java侧的方法进行匹配,这种方式是静态注册。
2.2 动态注册
步骤:
- 编写Java类,比如Hello.java,如下;
package learn.java.jni;
public class Hello {
static {
System.load("/Users/tongbo/Documents/Projects/jni/jnic/jnicppdemo/build/libJniSaysHello.dylib");
}
private static final int[] array = {1, 2, 3};
public static native String jniSaysHello();
private static native String jniHowdy();
protected static native String jniCalculateArrayLength(int[] array);
public static String javaSaysHello() {
return "Java: Hello JNI, this is Java!";
}
public static String javaHowdy() {
return "Java: How are you doing?";
}
public static String javaDoNotKnowArrayLenth() {
return "Java: Please help me calculate length of this array.";
}
public static void main(String[] args) {
// static register
System.out.println(javaSaysHello());
System.out.println(jniSaysHello());
// dynamic register
System.out.println(javaHowdy());
System.out.println(jniHowdy());
System.out.println(javaDoNotKnowArrayLenth());
System.out.println(jniCalculateArrayLength(array));
}
}
复制代码
- 在c/c++项目中添加jni.h头文件和jni_md.h头文件,这两个头文件是JDK自带的,在JAVA_HOME/include目录下,将这两个头文件拷贝到c/c++项目目录;
- 新建C/C++源文件dynamic_register.c。在该.c文件中,实现两个函数,这两个函数将是native方法在JNI的实现,如下:
#include "jni.h"
jstring howdy(JNIEnv *env, jclass jcls)
{
return (*env)->NewStringUTF(env, "JNI: I am fine, how are you?");
}
jstring calculate_array_len(JNIEnv *env, jclass jcls, jintArray arr)
{
int len = (*env)->GetArrayLength(env, arr);
char lenStr[100];
sprintf(lenStr, "JNI: Length of this array is %d.", len);
return (*env)->NewStringUTF(env, lenStr);
}
复制代码
- 到目前,Java的
jniHowdy
方法,jniCalculateArrayLength
方法与c/c++howdy
函数,calculate_array_len
还没有任何关联,我们需要手动管理关联; - 继续在dynamic_register.c内,新建一个以JNINativeMethod结构体为元素的数组,如下:
static const JNINativeMethod mMethods[] = {
{"jniHowdy", "()Ljava/lang/String;", (jstring *)howdy},
{"jniCalculateArrayLength", "([I)Ljava/lang/String;", (jstring *)calculate_array_len},
};
复制代码
- 以上数组中每一个元素,都是JNI侧实现方法与Java侧native方法的关联,前两个是Java侧native方法的描述,最后一个是JNI侧函数实现的描述,格式为:
{"Java侧的native方法名","方法的签名",函数指针}
复制代码
- 然后,我们需要实现jni.h中的JNI_OnLoad()方法,该方法的实现方法是一个模板,如下:
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv *env = NULL;
//获得 JNIEnv
int r = (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4);
if (r != JNI_OK)
{
return -1;
}
jclass mainActivityCls =
(*env)->FindClass(env, "learn/java/jni/Hello");
// 最后参数是需要注册的native方法的个数,如果小于0则注册失败。
r = (*env)->RegisterNatives(env, mainActivityCls, mMethods, 2);
if (r != JNI_OK)
{
return -1;
}
return JNI_VERSION_1_4;
}
复制代码
- 注意!第一:以上
FindClass(env, "learn/java/jni/Hello")
中的字符串是Java侧Hello类的全类名,注意此处的反斜杠/
;第二:RegisterNatives(env, mainActivityCls, mMethods, 2)
中的最后一个整型参数是需要动态注册的方法个数,手动添加注册或者删除注册都需要对应变化,当然可以直接把mMethod[]
的长度传进去,一劳永逸,如下:
int cnt = sizeof(mMethods)/ sizeof(mMethods[0]);
r = (*env)->RegisterNatives(env, mainActivityCls, mMethods, cnt);
复制代码
- 然后,配置CMakeList.txt,新增一个
add_library(JniHowdyAndCalculaterArrayLen SHARED dynamic_register.c)
,然后Cmake: Build
:
cmake_minimum_required(VERSION 3.0.0)
project(jnicppdemo VERSION 0.1.0)
include(CTest)
enable_testing()
# static register
add_library(JniSaysHello SHARED learn_java_jni_Hello.c learn_java_jni_Hello.h)
# dynamic register
add_library(JniHowdyAndCalculaterArrayLen SHARED dynamic_register.c)
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)
复制代码
- 在Hello.java源文件中,添加静态代码块,使用System.load()方法加载该动态链接库,如下:
package learn.java.jni;
public class Hello {
static {
System.load("/Users/tongbo/Documents/Projects/jni/jnic/jnicppdemo/build/libJniSaysHello.dylib");
System.load("/Users/tongbo/Documents/Projects/jni/jnic/jnicppdemo/build/libJniHowdyAndCalculaterArrayLen.dylib");
}
private static final int[] array = {1, 2, 3};
public static native String jniSaysHello();
private static native String jniHowdy();
protected static native String jniCalculateArrayLength(int[] array);
public static String javaSaysHello() {
return "Java: Hello JNI, this is Java!";
}
public static String javaHowdy() {
return "Java: How are you doing?";
}
public static String javaDoNotKnowArrayLength() {
return "Java: Please help me calculate length of this array.";
}
public static void main(String[] args) {
// static register
System.out.println(javaSaysHello());
System.out.println(jniSaysHello());
// dynamic register
System.out.println(javaHowdy());
System.out.println(jniHowdy());
System.out.println(javaDoNotKnowArrayLength());
System.out.println(jniCalculateArrayLength(array));
}
}
复制代码
- Java侧运行,效果如下:
- 动态注册成功。
system.load()与system.loadLibrary()的区别:
System.load()
System.load()参数必须为库文件的绝对路径, 例如: System.load("C:\Documents\TestJNI.dll"); //Windows System.load("/usr/lib/TestJNI.dylib"); //mac
System.loadLibrary()
System.loadLibrary()参数为库文件名,不包含库文件的扩展名。 System.loadLibrary("TestJNI"); //加载Windows下的TestJNI.dll本地库 System.loadLibrary("TestJNI"); //加载Linux下的libTestJNI.dylib本地库
注意:TestJNI.dll 或 libTestJNI.dylib 必须是在JVM属性java.library.path所指向的路径中。 loadLibary需要配置当前项目的java.library.path路径
3 JNI调用Java
3.1 JNI侧获取Java类运行时类
区分FindClass()与GetObjectClass()
- FindClass():JNI侧函数,相当于Java中的Class.forName(),通过Java侧的全类名获取到JNI可用的jclass变量。
jclass jclz =
(*env)->FindClass(env,"packageName/ClassName");
复制代码
- GetObjectClass():JNI侧函数,相当于Java侧的object.getClass(),通过从Java传到JNI侧的jobject对象类似于反射获取到该jobject对应的jclass变量。
jclass jclz = (*env)->GetObjectClass(env,jobj);
复制代码
3.2 JNI调用Java
调用字段
Java侧的类的字段分为非静态字段与静态字段,JNI有对应的GetXxx()
/SetXxx()
、GetStaticXxx()
/SetStaticXxx()
方法,分为以下几个步骤:
- JNI注册:在JNI侧注册Java侧的native方法;
- 获取jclass:如果注册的是static的native方法,则在JNI实现中直接传入了jclass变量,可直接跳到下一个步骤;如果注册的是非static的native方法,则在JNI实现中传入了jobject变量,需要通过GetObjectClass()方法获取到jclass;
- 获取jfieldID:调用GetFieldID()/GetStaticFieldID();
- 获取到字段:调用GetXxxField()/GetStaticXxxField();
- 操作字段:调用SetXxxField()/SetStaticXxxField()。
// java
package learn.java.jni;
public class JNICallJavaField {
static {
System.load("/Users/tongbo/Documents/Projects/jni/jnic/jnicppdemo/build/libJniCallJavaField.dylib");
}
public int field1 = 1;
public static int field2 = 8;
public native void func1();//不传入field1,但是希望JNI能修改field1
public static native void func2();//不传入field2,但是希望JNI能修改field2
public static void main(String[] args) {
JNICallJavaField jniCallJavaField = new JNICallJavaField();
jniCallJavaField.func1();
jniCallJavaField.func2();
System.out.println(jniCallJavaField.field1);
System.out.println(jniCallJavaField.field2);
}
}
复制代码
// c/c++
#include "learn_java_jni_JNICallJavaField.h"
/*
* Class: learn_java_jni_JNICallJava
* Method: func1
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_learn_java_jni_JNICallJavaField_func1(JNIEnv *env, jobject jobj)
{
jclass jclz = (*env)->GetObjectClass(env, jobj);
jfieldID jfieldId = (*env)->GetFieldID(env, jclz, "field1", "I");
jint field1 = (*env)->GetIntField(env, jobj, jfieldId);
(*env)->SetIntField(env, jobj, jfieldId, field1 + 10000);
}
/*
* Class: learn_java_jni_JNICallJava
* Method: func2
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_learn_java_jni_JNICallJavaField_func2(JNIEnv *env, jclass jclz)
{
jfieldID jfieldId = (*env)->GetStaticFieldID(env, jclz, "field2", "I");
jint field2 = (*env)->GetStaticIntField(env, jclz, jfieldId);
(*env)->SetStaticIntField(env, jclz, jfieldId, field2 + 10000);
}
复制代码
# CMakeList.txt
cmake_minimum_required(VERSION 3.0.0)
project(jnicppdemo VERSION 0.1.0)
include(CTest)
enable_testing()
# static register
add_library(JniSaysHello SHARED learn_java_jni_Hello.c learn_java_jni_Hello.h)
# dynamic register
add_library(JniHowdyAndCalculaterArrayLen SHARED dynamic_register.c)
# jni call java field
add_library(JniCallJavaField SHARED learn_java_jni_JNICallJavaField.c)
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)
复制代码
Java侧运行效果:
调用方法
注意:反射无法反射抽象方法,也没有必要反射抽象方法
在JNI看来,Java侧的方法分为:构造方法/静态方法/非静态方法。
Java侧代码
在进入JNI侧调用Java方法之前,先展示Java侧代码:
// 一个与native方法无关的类,包含一个空参构造方法,一个非静态方法,一个静态方法。
package learn.java.jni;
public class JNICallJavaMethod {
public JNICallJavaMethod(){
System.out.println("Java: I am constructor.");
}
public int func(int i){
System.out.println("Java: func is called.");
return i+100;
}
public static int staticFunc(int[] a){
System.out.println("Java: staticFunc is called.");
int sum = 0;
for(int i=0;i<a.length;i++){
sum += a[i];
}
return sum;
}
}
复制代码
// 一个包含native方法的类,还包含一个非静态方法。
package learn.java.jni;
public class JNICallJavaMethodNative {
static {
System.load("/Users/tongbo/Documents/Projects/jni/jnic/jnicppdemo/build/libJniCallJavaMethod.dylib");
}
public native int func1(int i);
public String func2() {
System.out.println("Java: I am func2 in the same class with the native func1.");
return "Java: Hi JNI.";
}
public static void main(String[] args) throws InterruptedException {
JNICallJavaMethodNative jniCallJavaMethodNative = new JNICallJavaMethodNative();
int res = jniCallJavaMethodNative.func1(8);
/**
* 测试JNI侧的控制台打印与Java侧的控制台打印之间的顺序关系:
* 结果发现,JNI侧的控制台打印一定是在Java之后,sleep也没用。
*/
Thread.sleep(2000);
System.out.println("Java: Result of native func1 is " + res);
}
}
复制代码
如上述代码所示,我们有两个类,第一个类JNICallJavaMethod
与native方法无直接关系,里面提供了构造方法,非静态方法,静态方法,供JNI调用。第二个类JNICallJavaMethodNative
包含了native
方法的声明,还包含了一个非静态方法供JNI调用;同时第二个类JNICallJavaMethodNative
提供psvm
作为整个程序的运行入口,程序开始运行后,在Java侧仅调用了native
方法,得到了以下的结果:
JNI侧代码
#include <stdio.h>
#include "jni.h"
#include "learn_java_jni_JNICallJavaMethodNative.h"
JNIEXPORT jint JNICALL Java_learn_java_jni_JNICallJavaMethodNative_func1(JNIEnv *env, jobject jobj, jint ji)
{
// JNI侧创建一个int array
jintArray jArr = (*env)->NewIntArray(env, 4);
jint *arr = (*env)->GetIntArrayElements(env, jArr, NULL);
arr[0] = 0;
arr[1] = 10;
arr[2] = 20;
arr[3] = 30;
(*env)->ReleaseIntArrayElements(env, jArr, arr, 0);
// 调用另一个类的构造函数
jclass jclz1 = NULL;
jclz1 = (*env)->FindClass(env, "learn/java/jni/JNICallJavaMethod");
if (jclz1 == NULL)
{
printf("JNI: jclz is NULL.");
return ji;
}
jmethodID jmethodId1 = NULL;
jmethodId1 = (*env)->GetMethodID(env, jclz1, "<init>", "()V");
if (jmethodId1 == NULL)
{
printf("JNI: jmethodId1 is NULL.");
return ji;
}
jobject jobj1 = (*env)->NewObject(env, jclz1, jmethodId1);
(*env)->CallVoidMethod(env, jobj1, jmethodId1);
// 调用另一个类的非静态方法
jmethodID jmethodId2 = NULL;
jmethodId2 = (*env)->GetMethodID(env, jclz1, "func", "(I)I");
if (jmethodId2 == NULL)
{
return ji;
}
jobject jobj2 = (*env)->NewObject(env, jclz1, jmethodId2);
jint i1 = (*env)->CallIntMethod(env, jobj2, jmethodId2, 5);
printf("JNI: func returns %d.\n", i1);
// 调用另一个类的静态方法
jmethodID jmethodId3 = NULL;
jmethodId3 = (*env)->GetStaticMethodID(env, jclz1, "staticFunc", "([I)I");
if (jmethodId3 == NULL)
{
printf("JNI: jmethodId3 is NULL.");
return ji;
}
jint i2 = (*env)->CallStaticIntMethod(env, jclz1, jmethodId3, jArr);
printf("JNI: staticFunc returns %d.\n", i2);
// 调用与native方法同属一个类的方法
jclass jclz0 = NULL;
jclz0 = (*env)->GetObjectClass(env, jobj);
if (jclz0 == NULL)
{
printf("JNI: jclz0 is NULL.");
return ji;
}
jmethodID jmethodId4 = NULL;
jmethodId4 = (*env)->GetMethodID(env, jclz0, "func2", "()Ljava/lang/String;");
if (jmethodId4 == NULL)
{
printf("JNI: jmethodId4 is NULL.");
return ji;
}
// 接收Java方法返回的字符串,并在JNI侧打印
jstring jstr = (jstring)(*env)->CallObjectMethod(env, jobj, jmethodId4);
char *ptr_jstr = (char *)(*env)->GetStringUTFChars(env, jstr, 0);
printf("JNI: func2 returns %s\n", ptr_jstr);
return ji + 100;
}
复制代码
显然,这段JNI代码是native方法在JNI侧的实现,其中先后
- 创建了一个jintArray;
- 调用了Java侧JNICallJavaMethod类的构造方法;
- JNICallJavaMethod类的非静态方法;
- JNICallJavaMethod类的静态方法;
- JNICallJavaMethodNative类的非静态方法。
我们分别分析,以下代码就是上述代码的分别分析。
调用构造函数
步骤:
- 加载类,被调用方法所在类的运行时类,即
jclass
; - 获取方法ID,即
jmethodID
; - 创建一个类的实例,即
jobject
; - 调用方法。
注意:
- MethodName形参直接传
<init>
即可; - 构造函数在Java侧没有返回值,连void都不是;但是,在JNI侧的方法签名中,返回值是void。比如,一个类的默认构造函数的签名是
()V
; - 凡是JNI方法的
GetXxx()
过程,都必须进行判空处理。
代码:
// 调用另一个类的构造函数
jclass jclz1 = NULL;
jclz1 = (*env)->FindClass(env, "learn/java/jni/JNICallJavaMethod");
if (jclz1 == NULL)
{
printf("JNI: jclz is NULL.");
return ji;
}
jmethodID jmethodId1 = NULL;
jmethodId1 = (*env)->GetMethodID(env, jclz1, "<init>", "()V");
if (jmethodId1 == NULL)
{
printf("JNI: jmethodId1 is NULL.");
return ji;
}
jobject jobj1 = (*env)->NewObject(env, jclz1, jmethodId1);
(*env)->CallVoidMethod(env, jobj1, jmethodId1);
复制代码
调用非静态方法
步骤:
- 加载类,被调用方法所在类的运行时类,即jclass;
- 获取方法ID,即jmethodID;
- 创建一个类的实例,即jobject;
- 调用方法。
注意:
- 因为该非静态方法与上述构造方法同属一个类,所以此时可以省去加载运行时类的步骤一,直接用已经获取到的jclass;
- Java侧来说,一个类的对象可以调用多个方法;但是JNI侧的jobject是与jmethodID一一对应的。所以,即使JNI侧调用的不同方法属于同一个类,也需要创建不同的jobject,不能共用;从创建jobject的JNI函数可以看出来:
//jobject与jmethodID是一一对应的关系:
jobject jobj1 = (*env)->NewObject(env, jclz1, jmethodId1);
复制代码
代码:
// 调用另一个类的非静态方法
jmethodID jmethodId2 = NULL;
jmethodId2 = (*env)->GetMethodID(env, jclz1, "func", "(I)I");
if (jmethodId2 == NULL)
{
return ji;
}
jobject jobj2 = (*env)->NewObject(env, jclz1, jmethodId2);
jint i1 = (*env)->CallIntMethod(env, jobj2, jmethodId2, 5);
printf("JNI: func returns %d.\n", i1);
复制代码
调用静态方法
步骤:
- 加载类,被调用方法所在类的运行时类,即jclass;
- 获取方法ID,即jmethodID;
- 调用方法。
注意:
- 因为该非静态方法与上述构造方法同属一个类,所以此时可以省去加载运行时类的步骤一,直接用已经获取到的jclass;
- 因为静态方法的调用不需要对象实例,所以调用Java静态方法时,不需要jobject。
代码:
// 调用另一个类的静态方法
jmethodID jmethodId3 = NULL;
jmethodId3 = (*env)->GetStaticMethodID(env, jclz1, "staticFunc", "([I)I");
if (jmethodId3 == NULL)
{
printf("JNI: jmethodId3 is NULL.");
return ji;
}
jint i2 = (*env)->CallStaticIntMethod(env, jclz1, jmethodId3, jArr);
printf("JNI: staticFunc returns %d.\n", i2);
复制代码
调用native方法所在类的方法
这里以非静态方法为例,因为Java侧方法与native方法在同一个类中,而JNI侧实现native方法时,会传入一个jclass
/jobject
,分别对应Java侧的native方法声明是static native
/native
。此时我们可以直接使用传入的jclass
,或者利用(*env)->GetObjectClass(env,jobj)
获取到运行时类。关键在于,调用哪个方法,首先需要加载该方法所在的类到JVM运行时环境中。
代码:
// 调用与native方法同属一个类的方法
jclass jclz0 = NULL;
jclz0 = (*env)->GetObjectClass(env, jobj);
if (jclz0 == NULL)
{
printf("JNI: jclz0 is NULL.");
return ji;
}
jmethodID jmethodId4 = NULL;
jmethodId4 = (*env)->GetMethodID(env, jclz0, "func2", "()Ljava/lang/String;");
if (jmethodId4 == NULL)
{
printf("JNI: jmethodId4 is NULL.");
return ji;
}
// 接收Java方法返回的字符串,并在JNI侧打印
jstring jstr = (jstring)(*env)->CallObjectMethod(env, jobj, jmethodId4);
char *ptr_jstr = (char *)(*env)->GetStringUTFChars(env, jstr, 0);
printf("JNI: func2 returns %s\n", ptr_jstr);
复制代码
Q&A
JNI侧如何创建整形数组
步骤:
- 声明数组名字与数组长度,即jArr、4;
- 获取数组元素类型(jint型)的指针,通过调用(*env)->GetIntArrayElements(env, jArr, NULL);
- 利用指针,为元素赋值;
- 释放指针资源,数组得以保留。
代码:
//todo:JNI侧创建一个int array
jintArray jArr = (*env)->NewIntArray(env, 4);//步骤1
jint *arr = (*env)->GetIntArrayElements(env, jArr, NULL);//步骤2
arr[0] = 0;//步骤3
arr[1] = 10;
arr[2] = 20;
arr[3] = 30;
(*env)->ReleaseIntArrayElements(env,jArr,arr,0);//步骤4
复制代码
Java侧方法返回String,JNI调用时如何打印返回值?
步骤:
- 定义jstring变量,并用(jstring)强转jobject;
- 定义字符型指针,并用 (char *)强转;
- 打印。
// 接收Java方法返回的字符串,并在JNI侧打印
jstring jstr = (jstring)(*env)->CallObjectMethod(env, jobj, jmethodId4);
char *ptr_jstr = (char *)(*env)->GetStringUTFChars(env, jstr, 0);
printf("JNI: func2 returns %s\n", ptr_jstr);
复制代码
JNI侧与Java侧的控制台打印顺序
结论是:
JNI侧的控制台打印一定出现在Java侧程序运行结束之后。
我们可以调试看现象:
两遍I am constructor?
答:在调用构造方法和非静态方法的两个调用过程中,都需要通过(*env)->NewObject(env, jclz, jmethodId)
创建与jmethodID一一对应的jobject,所以调用了两次构造函数。
两遍func is called?
答:待解答!
能否脱离native方法的实现来调用Java侧方法?
答:可以,JNI是Java跨平台的实现机制,是Java与原生代码交互的机制。上述的过程我们一般都是在JNI侧的native方法实现中进行的,因为native方法的JNI实现中就有JNIEnv*指针,是获取JNIEnv*最容易的方式,并非唯一方式。如何获取JNIEnv*?待解答!
3.3 JNI处理从Java传来的字符串
Java与C字符串的区别
- Java内部使用的是utf-16 16bit 的编码方式;
- JNI里面使用的utf-8 unicode编码方式,英文是1个字节,中文3个字节;
- C/C++ 使用ASCII编码,中文的编码方式GB2312编码,中文2个字节。
实战代码
//Java:
package JNICallJava;
public class GetSetJavaString {
static {
System.load("E:\\_Projects_\\JNI_Projects\\JNI_C\\cmake-build-debug\\libGetSetJavaStringLib.dll");
}
public static native String func(String s);
public static void main(String[] args) {
String str = func("--Do you enjoy coding?");
System.out.println(str);
}
}
复制代码
//C:
#include "stdio.h"
#include "jni.h"
JNIEXPORT jstring JNICALL Java_JNICallJava_GetSetJavaString_func
(JNIEnv *,jclass,jstring);//没有用专门的.h文件,此声明可写可不写。
JNIEXPORT jstring JNICALL Java_JNICallJava_GetSetJavaString_func
(JNIEnv *env,jclass jclz,jstring jstr){
const char *chr = NULL;//字符指针定义与初始化分开
jboolean iscopy;//判断jstring转成char指针是否成功
chr = (*env)->GetStringUTFChars(env,jstr,&iscopy);//&iscopy位置一般直接传入NULL就好
if(chr == NULL){
return NULL;//异常处理
}
char buf[128] = {0};//申请空间+初始化
sprintf(buf,"%s\n--Yes, I do.",chr);//字符串拼接
(*env)->ReleaseStringUTFChars(env,jstr,chr);//编程习惯,释放内存
return (*env)->NewStringUTF(env,buf);
}
复制代码
//CMakeLists.txt
add_library(GetSetJavaStringLib SHARED GetSetJavaString.c)
复制代码
运行结果
异常处理
上述代码实例中,GetStringUTFChars()方法将JNI的jstring变量转换成C语言能操作的char指针,这个过程可能失败,其实任何转换过程都可能失败,这些过程的目标变量的定义和初始化都需要分开进行,并通过判空进行异常处理。
C语言字符串拼接
在C语言中,没有String,字符串都是字符指针。其拼接过程不像Java等语言那么简单,分为以下过程:
- malloc申请空间
- 初始化
- 拼接字符串
- 释放内存
灵活的静态注册
- 此实战代码中,我们没有想一般的静态注册一样使用Java native产生的.h文件,而是直接在实现JNI方法之前写了一个JNI静态注册,这也是可行的,甚至这个提前的声明注册也是可以不写的。此时我们在CMakeLists.txt中的add_library()中值包含了该.c文件。核心在于add_library()中一定要包含native方法在JNI的实现函数,.h文件更多是Java命令生成的教你怎么写JNI实现的一个辅助,无关紧要。
- JNI无视Java侧的访问控制权限,但会区别静态或非静态。