Java与CC++交互JNI编程
哈哈,经过了前面几个超级枯燥的C、C++两语言的基础巩固之后,终于来了到JNI程序的编写了,还是挺不容易的,所以还得再接再厉,戒骄戒躁,继续前行!!
第一个JNI程序:
JNI是一种本地编程接口。它允许运行在JAVA虚拟机中的JAVA代码和用其他编程语言,诸如C语言、C++、汇编,写的应用和库之间的交互操作。
接着就开始咱们第一个“hello world”级别的JNI程序,咱们可以使用上了非常之亲切的Android Studio来做实验啦,为了说明JNI不是Android所独有的,咱们在新建工程的单元测试中来写我们的第一个JNI程序,先新建工程:
然后咱们第一个JNI实验不运行在手机上,而是采用单元测试来实验,如下:
目的不是为了装逼,而是为了说明JNI跟Android没有必然的联系,好先在这个单远测试用例中篇写第一个JNI方法,如下:
接下来就得实现该native方法啦,此时具体实现还是用clion来,所以将工具进行转向它:
其实在我们创建Android Studio带有NDK环境的项目就会自动生成一个CMakeLists文件,其中就有一个配置模板,上面就有对这个add_library的一个配置介绍,如下:
接下来咱们来编译一下就会生成一个可共享的动态库,如下:
好,接着就是在clion中来实现我们在Android Studio中定义的native方法,要实现首先得在cpp中引入<jni.h>才行,而该头文件是在JDK中,所以目前包含jni.h肯定是找不到的,如下:
此时需要在CMakeLists中配置一下该头文件的路径,我本地的JDK路径是在:
所以,在clion中的CMakeLists中加入该路径,如下:
此时头文件就可以找到不报错了:
然后咱们来编写具体实现,这里的函数规则就得按照JNI的命名来,这里由于是第一个程序,所以先不用关心太多细节,主要是能将整个流程跑通,由于需要在C++中以C的方式来运行,所以需要加如下关键字:
另外JNIEXPORT和JNICALL是固定模式,之后再解释,目前先按这个规则来,由于我们的native的定义原型为:
返回值为void,对应于:
然后方法名为test,在JNI中是需要以Java开头,然后加上方法的全限定名,以“_”来进行分隔,所以我们看到的方法就成这样了:
然后相比我们定义的方法之外,还多了两个固定的参数,这两个参数在未来学习中就能体会其用处滴,这里先忽略:
然后我们定义的两个参数因为是Java的,所以在cpp中则用jint和jstring来表示了:
然后具体实现中我们就来简单打印一下既可,其中我们可以看到string的获取是通过了env这个参数,未来的学习中很多操作都得依靠这个参数,这里就不多解释了:
【注意】:可能在编写该实现时会报错,如下:
这是因为头文件路径中还少了一个,加上就可以了,如下:
然后再编译生成动态库,此时就可以回到我们的Android Studio啦,需要先加载我们生成的动态库,具体做法如下:
然后咱们直接run这个case,看下效果:
这样我们第一个JNI程序就顺利跑通啦,还挺麻烦,其实用NDK开发没这么麻烦,之所以利用麻烦的方式主要是用来阐述JNI和NDK是两码事,JNI是一个标准,而NDK是Android用来编写底层代码提供的一套工具。
另外对于native方法对应的具体实现规则不懂的话,还有一个取巧的办法使用javah获得方法该如何声明。javah是JDK中提供的工具,回到咱们的Android Studio中,咱们来使用一下javah,如下:
然后将此头文件拷贝到clion当中:
然后再在cpp中引入该头文件对其函数具体实现既可:
JNI数据类型:
在我们编写的第一个JNI程序中,其中可以看到有两个比较奇怪的东东:
其中JNIEXPORT 和 JNICALL,定义在jni_md.h
头文件中:
下面具体来看一下:
JNIEXPORT:
在 Windows 中,定义为__declspec(dllexport)
。因为Windows编译 dll 动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加此标识,表示将该函数导出在外部可以调用。
在 Linux/Unix/Mac os/Android 这种 Like Unix系统中,定义为__attribute__ ((visibility ("default")))
GCC 有个visibility属性, 该属性是说, 启用这个属性:
-
当-fvisibility=hidden时
动态库中的函数默认是被隐藏的即 hidden. 除非显示声明为
__attribute__((visibility("default")))
. -
当-fvisibility=default时
动态库中的函数默认是可见的.除非显示声明为
__attribute__((visibility("hidden")))
.
JNICALL:
在类Unix中无定义,在Windows中定义为:_stdcall
,一种函数调用约定。
【注意】:类Unix系统中这两个宏可以省略不加。
接下来函数参数声明中还多了两个类型,如:
其中JNIEnv类型实际上代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。例如,创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等等。JNIEnv的指针会被JNI传入到本地方法的实现函数中来对Java端的代码进行操作。而jobject代表了Java对象,对于咱们这个程序就是指定义了该jni方法的类,如下:
其中java中的每一个数据类型在jni中都有对应的类型,下面具体列举一下:
Java类型 | 本地类型 | 描述 |
---|---|---|
boolean | jboolean | C/C++8位整型 |
byte | jbyte | C/C++带符号的8位整型 |
char | jchar | C/C++无符号的16位整型 |
short | jshort | C/C++带符号的16位整型 |
int | jint | C/C++带符号的32位整型 |
long | jlong | C/C++带符号的64位整型 |
float | jfloat | C/C++32位浮点型 |
double | jdouble | C/C++64位浮点型 |
Object | jobject | 任何Java对象,或者没有对应java类型的对象 |
Class | jclass | Class对象 |
String | jstring | 字符串对象 |
Object[] | jobjectArray | 任何对象的数组 |
boolean[] | jbooleanArray | 布尔型数组 |
byte[] | jbyteArray | 比特型数组 |
char[] | jcharArray | 字符型数组 |
short[] | jshortArray | 短整型数组 |
int[] | jintArray | 整型数组 |
long[] | jlongArray | 长整型数组 |
float[] | jfloatArray | 浮点型数组 |
double[] | jdoubleArray | 双浮点型数组 |
好,接下来利用NDK来做实验【当然也就抛弃了clion这个工具喽】,将其运行在手机上【这里采用模拟器】,下面来声明一个native方法:
其中它里面的CMakeLists.txt的配置跟我们第一个在clion中差不多,如下:
不过貌似目前这个文件的可读性比较差,都分不清哪些是注释,哪些是配置,所以这里可以给Android Studio增加一下"CMake simple highlighter"高亮工具,让其可读性增强,如下:
装完之后,再来查看我们的CMakeLists.txt文件的内容就长这样了:
注释跟配置一目了然~~好了,接下来我们得实现这个native方法,在Android Studio中其实现函数的定义可以用快捷键来生成了,不用像之前手动写或者通过javah来生成,具体做法如下:
接下来咱们首先在C++中来获取java传过的int[]数组,当然就不能直接用下标来访问了,此时就得用JNI的API先将jintArray转成jint,如下:
其中第二个参数传了一个NULL,这代表啥意思呢?看一下该方法的定义:
如果传true,则是拷贝的一个新数据(新申请内存),而如果传false就是使用java的数组,这里我们的是NULL,也就是false,直接使用Java传递过来的数据。接下来咱们来将该int数组进行遍历,如何做呢?
首先来获取数组的长度:
其中jsize就是jint:
而jint是:
接下来则进行遍历,将元素1个个打印出来:
但是!!由于运行在手机Android系统里,而非电脑上,所以用printf打印是没法在logcat中看到的,所以这里需要引用NDK提供的日志库,先引入头文件:
修改打印为:
不过通常会将日志的输出定义一个宏,方便进行调用,如下:
好,实现好了之后,咱们来在Activity中来调用一下它:
然后编译运行,看logcat中的输出:
这样就用C++的方式来对Java中的数组进行了遍历了,再回到cpp的代码中,发现在遍历之后还调用了一个释放操作,如下:
其中第三个参数需要注意一下,先看一下该函数的声明:
其值可以有0、1、2,其具体含义为:
0:刷新java数组,并释放c/c++数组。
1:JNI_COMMIT,只刷新java数组。
2:JNI_ABORT,只释放c/c++数组。
其中看下官方的注释:
下面咱们来试一下这个参数的作用,先在Activity中将int数组的元素打印出来以便观察:
先用2来试一下:
下面运行看一下:
那如果改为0或1呢?
编译运行:
可能是跟平台有关,反正实验在mac上没看出啥区别,先记着这个参数的区别吧。
下面来看一下细节,就是关于JNIEnv的参数,在C和C++的定义是不一样的,这样也就决定了在C和C++编写JNI函数时也会存在差别,先来看一下具体的定义:
在C++是如上定义的,咱们进一步跟踪:
它是一个结构体,所以我们在编写JNI的实现时,就可以直接这样写:
然后咱们再看一下C的定义:
此时该参数就变成了指针的指针了,如下:
所以此时在JNI实现时就得这样写了:
另外对于Android Studio编写的JNI那生成的.so的动态库是在哪里呢?平常我们项目中用到native一般都会引入so文件嘛,其实在这里:
接下来我们再对Java传过来的字符串数组进行遍历,如下:
然后咱们再将jstring打印出来,但是可惜不能直接打印,需要进行如下转换才行:
编译运行:
C/C++反射Java:
新定义一个native方法,传递一个我们自定义的类,如下:
然后还是通过快捷键来生成出具体的实现函数,如下:
反射方法:
接下来咱们来反射一下Java方法,首先得获取Java对象的Class对象才行,如下:
接着找到要调用的方法,具体如下:
关于第三个参数如何传,其实如果研究过jvm字节码相关的信息一下就秒懂,这里涉及到数据类型在字节码的表示,基本数据类型的签名采用一系列大写字母来表示, 具体如下表所示:
Java类型 | 签名 |
---|---|
boolean | Z |
short | S |
float | F |
byte | B |
int | I |
double | D |
char | C |
long | J |
void | V |
引用类型 | L + 全限定名 + ; |
数组 | [+类型签名 |
所以第三个参数如下:
如果说硬是不知道怎么写第三个的方法描述符,那可以用javap命令得到,如下:
下面具体来调用一下该方法:
编译运行:
同样的咱们还可以调用对象的setI()方法来改变对象里面的值,如下:
咱们在setI()方法中打一段日志用来观察:
编译运行:
接下来咱们调用一下静态方法试试,首先在Bean中增加一个静态方法:
然后类似的做法在c++当中调用它:
编译运行:
而如果方法中是自定义的类那如何调用呢,咱们新建一个类:
然后再到Bean中再声明一个静态方法,参数传这个Bean2类,如下:
然后咱们在cpp中来调用它,这个就稍麻烦一些,如下:
然后再用构造方法通过反射去生成对象:
运行一下:
反射属性:
也能够对类中的属性进行反射调用,用法基本跟反射方法的调用差不多,比如我们想反射Bean类中的i成员属性,如下:
编译运行:
另外注意一个细节,就是在我们使用一个引用时在最后都进行了delete,如下:
这里就涉及到了JNI引用相关的知识点了,所以下面来了解一下它。
JNI引用:
在 JNI 规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。
局部引用:
大多数JNI函数会创建局部引用。NewObject/FindClass/NewStringUTF 等等都是局部引用。局部引用只有在创建它的本地方法返回前有效,本地方法返回后,局部引用会被自动释放。因此无法跨线程、跨方法使用。
释放一个局部引用有两种方式:
1、本地方法执行完毕后VM自动释放;
2、通过DeleteLocalRef手动释放;
VM会自动释放局部引用,为什么还需要手动释放呢?
因为局部引用会阻止它所引用的对象被GC回收。如图所示:
拿我们写的程序来说:
全局引用:
全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效 。由 NewGlobalRef 函数创建,下面来先来看一下如下程序:
然后Bean2中增加一个get方法:
然后在c++中去生成Bean2对像,假如针对调多次的情况想这样写:
咱们此时运行肯定是木啥问题的,但是!!如果native方法调用两次呢?
这是为啥呢?这是因为不能在本地方法中把局部引用存储在全局变量中缓存起来供下一次调用时使用。第二次执行时,其实bean2Class依然有值,但是其指向的地址数据已经被释放,也就是bean2Class成了悬空指针,所以第二次再用它时肯定就抛异常了,那如果想达到这样的效果就得用JNI提供的全局引用了,具体如下:
如果全局引用不再使用了,可以手动用如下进行释放:
弱引用:
与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象【其实跟Java的WeakReference差不多】 。
在对Class进行弱引用是非常合适(FindClass),因为Class一般直到程序进程结束才会卸载。
在使用弱引用时,必须先检查缓存过的弱引用是指向活动的对象,还是指向一个已经被GC的对象
用代码进行说明,假如想把其中的一个局部引用变为全局变量供其它程序使用,此时就可以使用弱引用,如下:
当然弱引用是不会阻止GC回收的,所以当我们使用弱引用的对像可能被回收,所以使用时需要判断弱引用的对像是否被回收了,所以代码可以这样改成: