一篇文章带你快速了解JNI开发~

最近在学习JNI,以下是个人的学习笔记,当然写在这里也希望能给初学JNI的朋友帮助~

JNI概述

官方定义

做过安卓的都知道JNI,关于JNI Oracle文档给出的定义是:

The JNI is a native programming interface. It allows Java code that runs inside a Java Virtual Machine (VM) to interoperate with applications and libraries written in other programming languages, such as C, C++, and assembly.

The most important benefit of the JNI is that it imposes no restrictions on the implementation of the underlying Java VM. Therefore, Java VM vendors can add support for the JNI without affecting other parts of the VM. Programmers can write one version of a native application or library and expect it to work with all Java VMs supporting the JNI.

翻译过来简单来说就是JNI是一个允许Java代码和其他语言交互的编程接口,这里其他语言指的是C、C++、汇编等。

JNI最大的好处是它对JVM底层的实现不会有影响,这样JVM提供商在提供JNI支持功能的时候不会影响到JVM其他部分,程序员在编写JNI代码的时候只需要写一份本地程序(上面说的其他语言)就可以在不同的JVM运行。

为什么要使用JNI

Oracle文档又写道:

While you can write applications entirely in Java, there are situations where Java alone does not meet the needs of your application. Programmers use the JNI to write Java native methods to handle those situations when an application cannot be written entirely in Java.

The following examples illustrate when you need to use Java native methods:

The standard Java class library does not support the platform-dependent features needed by the application.
You already have a library written in another language, and wish to make it accessible to Java code through the JNI.
You want to implement a small portion of time-critical code in a lower-level language such as assembly.

By programming through the JNI, you can use native methods to:

扫描二维码关注公众号,回复: 10749087 查看本文章

Create, inspect, and update Java objects (including arrays and strings).
Call Java methods.
Catch and throw exceptions.
Load classes and obtain class information.
Perform runtime type checking.

就不一一翻译了,主要就是说使用JNI主要目的为以下:
1.使用系统平台依赖的功能
2.方便接入一个其他语言编写的库到Java代码中
3.使用类似汇编等底层语言编写的高性能代码
4.(文档中没提到的)增加代码被反编译的难度提高代码安全

然后又说了通过JNI可以对Java代码的操作,比如创建对象、更新对象、调用方法、捕捉异常、加载类信息、运行时类型检查等。

具体见Oracle文档:https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/intro.html#wp9502

前面经常提及C语言,所以本篇博文的本地代码也是用C语言写的,如果对C语言不熟悉建议先熟悉下C语言再来看博文。

编写第一行JNI代码,Java调用C方法得到一个字符串

1.编写带有native的Java方法:

Java代码我使用 IntelliJ IDEA编写,这里我使用C语言编写本地代码,使用Visual studio。

来,我们写一个JNITest的类,并写一个native方法:

public class JniTest {
    public native static String getStringFromC();
     }

native关键字告诉Java,这只是一个方法的声明,具体实现在C中。

2.编译Java类:

build一个project,生成class文件。

3.生成头文件:

然后在终端使用javah生成C语言头文件:

javah -classpath D:\Study\ideaProjects\out\production\ideaProjects -d D:\Study\ideaProjects\jni JniTest

可以看下Oracle对javah的描述:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javah.html

这里指定的classpath就是IDEA编译生成的class目录路径,-d后表示头文件输出路径,最后是类的全名

生成的头文件:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JniTest */

#ifndef _Included_JniTest
#define _Included_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JniTest
 * Method:    getStringFromC
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_JniTest_getStringFromC
  (JNIEnv *, jclass);

头文件是什么呢?它就是一个函数原型,用来映射Java本地方法和C中的函数。简单来说,就是Java调用某个本地方法的时候,通过头文件,JVM才可以找到C中对应的函数。

可以看到头文件中的函数定义很有规律:java + 类名 + 对应的Java本地方法名。

4.使用C++编写本地函数,并生成all库:

在Visual studio中创建C文件,把头文件中的方法

JNIEXPORT jstring JNICALL Java_JniTest_getStringFromC
  (JNIEnv *, jclass);

拷贝到C中,加上具体形参和一段基本代码(待会解释):

//访问成员方法
JNIEXPORT jstring JNICALL Java_JniTest_getString2FromC
(JNIEnv *env, jobject jobject){
	//将C字符串转为Java字符串
	return (*env)->NewStringUTF(env, "I am C String");
}

将生成的头文件添加到我们的C工程中的头文件,并在对应的C文件中include进来。
并将jdk目录下的include目录中的jni.h和jni_md.h添加到C工程中的头文件。

简单介绍下上面的代码。返回类型的jstring 是什么呢?首先了解下JNI本地类型。
JNI本地类型说白了就是Java的原始类型对应的C中的类型,Java是平台无关语言,不同平台每种数据类型占用内存都是一样的,而C语言不同平台同种数据类型占用空间可能不同,所以需要一种机制来消除这两种语言之间的差异。以下是一张原始类型的映射表:

从表中可知,jstring就是代表了Java中的String类型。

另外非原始类型也提供了相应的映射:

另外对象类型之间的关系如图:

再看下参数JNIEnv *env,它是一个指向JNI函数表的指针,表中的每个元素都指向一个JNI函数,而本地函数必须通过JNI函数来访问JVM中的数据,即C必须通过JNIEnv的指针来访问Java。

第二个参数 jobject jobject看第二张表就可以知道,它代表Java一个对象。在这里,该方法对应的Java本地方法是一个成员方法,这里传进来的就是调用方法的对象所对应的JNI本地类型对象。

看下函数体,非常简单:

 (*env)->NewStringUTF(env, "I am C String");

这里调用了一个JNI函数NewStringUTF:
文档的定义是:
Constructs a new java.lang.String object from an array of characters in modified UTF-8 encoding.

C工程生成dll文件,并将所在的路径添加到环境变量path中。

一看就懂了。
总的来说该函数的功能就是返回了 "I am C String"给Java。

在Java代码中调用该native方法并打印出返回值:

String b = t.getString2FromC();
        System.out.println(b);

运行可以看到成功打印出了:
I am C String

访问Java成员变量

按照前面的套路,给JNITest添加属性s和native方法accessField:

private String s = "kkk";
public native String accessField();

然后使用javah生成头文件:

JNIEXPORT jstring JNICALL Java_JniTest_accessField
  (JNIEnv *, jobject);

看起来和前面一个头文件方法基本一样~

在C文件中实现该方法:

//修改对象属性
JNIEXPORT jstring JNICALL Java_JniTest_accessField
(JNIEnv *env, jobject jobject){
	//获得调用方法对象对应的class对象对应的jclass对象
	jclass cls = (*env)->GetObjectClass(env,jobject);
	//获得属性id
	jfieldID fiedlId = (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;");
	//获得属性值
	jstring string = (*env)->GetObjectField(env, jobject, fiedlId);
	//将获得的jstring转化为C语言的字符串数组
	char *s = (*env)->GetStringUTFChars(env, string, NULL);
	//创建新的C字符串
	char text[20] = "super";
	//新的字符串和原来的进行连接
	strcat(text, s);
	//将连接后的字符串转化为jstring
	jstring result = (*env)->NewStringUTF(env, text);
	//将上面的结果赋值给Java对应的属性
	(*env)->SetObjectField(env, jobject, fiedlId, result);

	return result;
}

通过JNI在C中修改Java的属性总体的步骤:

1.获得调用方法的对象对应的jclass对象,这里使用了GetObjectClass方法来获取。
2.调用GetFieldID函数,通过jclass对象和属性名以及属性签名获得对应的属性ID。
关于GetFieldID,看下官方文档:
Returns the field ID for an instance field of a class. The field is
specified by its name and descriptor. The GetField and
SetField families of accessor functions use field IDs to
retrieve instance fields. The field must be accessible from the
class referred to by clazz . The actual field, however, may be
defined in one of clazz ’s superclasses. The clazz reference
must not be NULL .
GetFieldID causes an uninitialized class to be initialized.
GetFieldID cannot be used to obtain the length of an array. Use
GetArrayLength instead.

关于参数这里需要解释两个概念:
属性签名相当于属性的描述符,它由属性的类型决定,基本类型的签名如下图:

引用类型如图:

自定义的类因为不固定,所以签名也不固定,可有通过javap命令查看类所有的属性方法的签名。

在这里因为属性是字符串,所以签名为Ljava/lang/String;。

field Id相当于Java对象中的属性在C中的一个身份证,通过它C才可以拿到属性对应的值:

jstring string = (*env)->GetObjectField(env, jobject, fiedlId);

通过field id得到的属性值是以jstring类型表示的,因为这是C从java获取的数据。这里因为C是不能直接操作jstring的,所以要修改属性还要将jstring转化为C的字符串:

char *s = (*env)->GetStringUTFChars(env, string, NULL);

然后就是字符串的处理,生成一个新的字符串,要将该新字符串设置给Java,还要将这个字符串转化为jstring,然后通过SetObjectField方法设置。

还是用刚才的方式,在Java中调用accessField方法,成功修改了s的值。

修改静态成员变量

在Java代码中添加静态变量和native方法:

private static int count;
public native int accessStaticField();

生成头文件得到方法:

JNIEXPORT jint JNICALL Java_JniTest_accessStaticField
		(JNIEnv *, jobject);

拷贝到C工程并实现该方法:

JNIEXPORT jint JNICALL Java_JniTest_accessStaticField
(JNIEnv *env, jobject jobject){
	jclass cls = (*env)->GetObjectClass(env, jobject);
	jfieldID fiedlId = (*env)->GetStaticFieldID(env, cls, "count", "I");
	jint count = (*env)->GetStaticIntField(env, cls, fiedlId);
	count++;
	(*env)->SetStaticIntField(env, cls, fiedlId, count);

	return count;
}

可以看出几乎和修改成员变量一样的流程,只是相应的函数变成了对应的static
方法,这里就不重复讲解了。

调用Java中的成员方法

还是在Java添加native方法:

public native void accessMethod();

然后还是生成头文件以及在C中实现对应的方法:

//访问方法
JNIEXPORT void JNICALL Java_JniTest_accessMethod
(JNIEnv *env, jobject jobject){
	jclass cls = (*env)->GetObjectClass(env, jobject);
	jmethodID mid = (*env)->GetMethodID(env, cls, "printNum", "(I)I");
	(*env)->CallIntMethod(env, jobject, mid,5);
}

哈哈,套路是不是非常明显?都是拿到对应的ID之后调用相应的方法就可以了。注意GetMethodID中传入的签名为返回值类型,
CallXXXMethod代表函数的返回值类型为XXX,从第四个参数开始为被调用函数的传参列表。

调用Java中的构造方法

同样在Java中添加native方法:

public native long accessConstructor();

在C中实现方法:

JNIEXPORT jlong JNICALL Java_JniTest_accessConstructor
(JNIEnv *env, jobject object){
	//要调用一个类的构造方法,首先拿到这个类的jclass对象
	jclass cls = (*env)->FindClass(env, "java/util/Date");
	//注意构造方法名传入的是<init>
        jmethodID constructor = (*env)->GetMethodID(env, cls,"<init>","()V");
	//通过构造方法创建一个对象
	jobject date = (*env)->NewObject(env, cls, constructor);
	//调用创建的对象的方法
	jmethodID getTimeId = (*env)->GetMethodID(env, cls, "getTime", "()J");
	jlong time = (*env)->CallLongMethod(env, date, getTimeId);

	printf("time:%lld\n", time);
	return time;
}

这里调用了FindClass去获取该类对应的jclass对象,需要传入类的全名,然后获取构造方法的方法ID,注意这里构造方法的ID的获取传入的方法名是,这也是和获取其他方法ID的不同之处。至于签名的获取还是一样的。

获取到jobject对象之后,就如同访问方法一样,调用该对象的一个方法。

访问Java中的数组

同样在Java中添加native方法:

public native String getArray(int s[]);

C中实现的方法:

JNIEXPORT jstring JNICALL Java_JniTest_getArray
(JNIEnv *env, jobject object, jintArray array){
	//将jintArray转化为C中的数组
	jint *myArray = (*env)->GetIntArrayElements(env, array, NULL);
	int len = (*env)->GetArrayLength(env, array);
	//排序的是c数组
	qsort(myArray, len, sizeof(jint), compare);
	//c数组同步到java数组
	(*env)->ReleaseIntArrayElements(env, array, myArray,0);

}

int compare(int *a, int *b){
	return (*a) - (*b);
}

因为要修改数组,所以要先将jintArray转化为C中的数组,然后才可以调用qsort方法对其进行修改。

最后又调用了ReleaseIntArrayElements来释放C中的数组和完成C数组到Java数组的同步。

关于ReleaseIntArrayElements函数,看下相关文档描述:

void ReleaseArrayElements(JNIEnv *env,
ArrayType array, NativeType *elems, jint mode);

A family of functions that informs the VM that the native code no longer needs access to elems. The elems argument is a pointer derived from array using the corresponding GetArrayElements() function. If necessary, this function copies back all changes made to elems to the original array.

可以看出该系列方法主要是为了通知JVM本地中的数组已经不需要指向Java的数组了。这里要注意mode参数,文档描述:

The mode argument provides information on how the array buffer should be released. mode has no effect if elems is not a copy of the elements in array. Otherwise, mode has the following impact, as shown in the following:

0 : copy back the content and free the elems buffer
JNI_COMMIT : copy back the content but do not free the elems buffer
JNI_ABORT: free the buffer without copying back the possible changes

因为我选择了0,所以是同步到Java数组并释放C数组。

最后文档给的建议:
In most cases, programmers pass “0” to the mode argument to ensure consistent behavior for both pinned and copied arrays. The other options give the programmer more control over memory management and should be used with extreme care.

即一般情况下都是传0,除非要搞特殊~

局部全局引用

JNI的引用目的是为了告诉JVM何时可以回收一个对象。(类似Java中的强引用)

在JNI中一共有三种引用:
局部引用:方法中的引用,在方法结束后回收。
全局引用:方法外的引用,跨方法跨线程共享。
弱引用:和全局引用很相似,不同在于它不能保证引用的对象不会回收(类似Java中的弱引用)

局部引用可以用DeleteLocalRef方法来提前释放内存,一般在优化内存时用到。

全局引用的创建和释放:

//全局引用
jstring globalString;

JNIEXPORT void JNICALL Java_JniTest_createGlobalReference
(JNIEnv *env, jobject object){
	jstring globalObject = (*env)->NewStringUTF(env,"hello");
	globalString = (*env)->NewGlobalRef(env, globalObject);
}

/*
* Class:     JniTest
* Method:    getGlobalReference
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JniTest_getGlobalReference
(JNIEnv *env, jobject object){
	return globalString;
}

/*
* Class:     JniTest
* Method:    deleteGlobalReference
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JniTest_deleteGlobalReference
(JNIEnv *env, jobject object){
	//释放全局引用
	(*env)->DeleteGlobalRef(env, globalString);
}

一旦全局引用被释放,Java访问的时候会报空指针异常。

这就是我最近学习JNI的笔记,接下来会学习ndk方面的东西,也会有相关笔记出炉~~

发布了69 篇原创文章 · 获赞 76 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/sinat_23092639/article/details/84451034