java jni编程_Java与CC++交互JNI编程

哈哈,经过了前面几个超级枯燥的C、C++两语言的基础巩固之后,终于来了到JNI程序的编写了,还是挺不容易的,所以还得再接再厉,戒骄戒躁,继续前行!!

第一个JNI程序:

JNI是一种本地编程接口。它允许运行在JAVA虚拟机中的JAVA代码和用其他编程语言,诸如C语言、C++、汇编,写的应用和库之间的交互操作。

接着就开始咱们第一个“hello world”级别的JNI程序,咱们可以使用上了非常之亲切的Android Studio来做实验啦,为了说明JNI不是Android所独有的,咱们在新建工程的单元测试中来写我们的第一个JNI程序,先新建工程:

529016f8e7820e912f1d4d4177694a4b.png

11c231fb91b95f4ab6320cb193896a11.png

然后咱们第一个JNI实验不运行在手机上,而是采用单元测试来实验,如下:

4a6d88651972d9d3be0f500e113020ef.png

目的不是为了装逼,而是为了说明JNI跟Android没有必然的联系,好先在这个单远测试用例中篇写第一个JNI方法,如下:

0306b5a17b0d81733bfa3402cc254444.png

接下来就得实现该native方法啦,此时具体实现还是用clion来,所以将工具进行转向它:

dbf9df3b2a25620d77ca8ce82c1d661e.png

66dce90278aab9fdfd2b6080f45bfd25.png

其实在我们创建Android Studio带有NDK环境的项目就会自动生成一个CMakeLists文件,其中就有一个配置模板,上面就有对这个add_library的一个配置介绍,如下:

ff887c1f64045503bc14a9465e8eb72f.png

接下来咱们来编译一下就会生成一个可共享的动态库,如下:

24608fdc4f046cf467a2bf459a120a46.png

好,接着就是在clion中来实现我们在Android Studio中定义的native方法,要实现首先得在cpp中引入才行,而该头文件是在JDK中,所以目前包含jni.h肯定是找不到的,如下:

f51677fe4a576c996d8c97ce68fb4fc7.png

此时需要在CMakeLists中配置一下该头文件的路径,我本地的JDK路径是在:

8556c7a9a885414d6bf0d93543e4b01d.png

8bec5a9a0279c447a776b3f36a7705c3.png

所以,在clion中的CMakeLists中加入该路径,如下:

2799cea6f87c02ff02ce228ae0bcf809.png

此时头文件就可以找到不报错了:

e93dbfe762b26959b102586b8160c984.png

然后咱们来编写具体实现,这里的函数规则就得按照JNI的命名来,这里由于是第一个程序,所以先不用关心太多细节,主要是能将整个流程跑通,由于需要在C++中以C的方式来运行,所以需要加如下关键字:

dced58e3d8e2bc9b22854e2e88a8d12b.png

另外JNIEXPORT和JNICALL是固定模式,之后再解释,目前先按这个规则来,由于我们的native的定义原型为:

6c457fdf78e177e7a01d7cb620a8e723.png

返回值为void,对应于:

70e75cb65c3953cea6de3ee5032172c5.png

然后方法名为test,在JNI中是需要以Java开头,然后加上方法的全限定名,以“_”来进行分隔,所以我们看到的方法就成这样了:

42b0ced6bd188e3601cbd298dc8f072a.png

然后相比我们定义的方法之外,还多了两个固定的参数,这两个参数在未来学习中就能体会其用处滴,这里先忽略:

85915e2cc49e93a663639b9c02425ace.png

然后我们定义的两个参数因为是Java的,所以在cpp中则用jint和jstring来表示了:

3b251b1e1f132110b51acfd0ef2b4ca4.png

然后具体实现中我们就来简单打印一下既可,其中我们可以看到string的获取是通过了env这个参数,未来的学习中很多操作都得依靠这个参数,这里就不多解释了:

2b324b0982b3bd7b56cb455b5567fde9.png

【注意】:可能在编写该实现时会报错,如下:

6432514839160500dcfe15ad76504e01.png

这是因为头文件路径中还少了一个,加上就可以了,如下:

14568a0046abc77e9dc783346ba5c0de.png

c14375f576ffc4217d153c789c5617ac.png

然后再编译生成动态库,此时就可以回到我们的Android Studio啦,需要先加载我们生成的动态库,具体做法如下:

f0b066c37491a23302705929b95e6925.png

然后咱们直接run这个case,看下效果:

3d82b5c3e3bcdd8131625fff7e9ca106.gif

这样我们第一个JNI程序就顺利跑通啦,还挺麻烦,其实用NDK开发没这么麻烦,之所以利用麻烦的方式主要是用来阐述JNI和NDK是两码事,JNI是一个标准,而NDK是Android用来编写底层代码提供的一套工具。

另外对于native方法对应的具体实现规则不懂的话,还有一个取巧的办法使用javah获得方法该如何声明。javah是JDK中提供的工具,回到咱们的Android Studio中,咱们来使用一下javah,如下:

4e06c24781858f473dec6cce2a724f14.png

然后将此头文件拷贝到clion当中:

a5660bf567e99b71e8b77f6ac44633d5.png

c9840980dfc29efe5457609312ddece5.png

然后再在cpp中引入该头文件对其函数具体实现既可:

863008b33d096d55291204d5b52cdc97.png

JNI数据类型:

在我们编写的第一个JNI程序中,其中可以看到有两个比较奇怪的东东:

2bfa38a65380fcf4643b7396df2190cb.png

其中JNIEXPORT 和 JNICALL,定义在jni_md.h头文件中:

aafd6cd86e4d70f85e5f6b84936fdbb8.png

下面具体来看一下:

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系统中这两个宏可以省略不加。

接下来函数参数声明中还多了两个类型,如:

c6030649de2c424bbe56157cd2bbf09e.png

其中JNIEnv类型实际上代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。例如,创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等等。JNIEnv的指针会被JNI传入到本地方法的实现函数中来对Java端的代码进行操作。而jobject代表了Java对象,对于咱们这个程序就是指定义了该jni方法的类,如下:

2e85675d961e6e62ed0bb69a1f4619f1.png

其中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方法:

e415cdce3f5320a3e554bd7b742a0790.png

其中它里面的CMakeLists.txt的配置跟我们第一个在clion中差不多,如下:

1fbdd7b86ba357c081d981cbd665328c.png

不过貌似目前这个文件的可读性比较差,都分不清哪些是注释,哪些是配置,所以这里可以给Android Studio增加一下"CMake simple highlighter"高亮工具,让其可读性增强,如下:

d23a90ca56d01be2d5deae630cf3fa5d.png

装完之后,再来查看我们的CMakeLists.txt文件的内容就长这样了:

fa182f1011c714d5d5dc5845db7de38e.png

注释跟配置一目了然~~好了,接下来我们得实现这个native方法,在Android Studio中其实现函数的定义可以用快捷键来生成了,不用像之前手动写或者通过javah来生成,具体做法如下:

be00657e89cbebf39f36a7fafb161455.png

d0ba818202bc31e5fef3318b791c8e62.png

接下来咱们首先在C++中来获取java传过的int[]数组,当然就不能直接用下标来访问了,此时就得用JNI的API先将jintArray转成jint,如下:

7dd422c68966454de802d7ff8c37253a.png

其中第二个参数传了一个NULL,这代表啥意思呢?看一下该方法的定义:

5763694eb6866269c5046a764b10ba90.png

如果传true,则是拷贝的一个新数据(新申请内存),而如果传false就是使用java的数组,这里我们的是NULL,也就是false,直接使用Java传递过来的数据。接下来咱们来将该int数组进行遍历,如何做呢?

首先来获取数组的长度:

e962200d0e071509512a2b3c2b8296ff.png

其中jsize就是jint:

bed1883489e03d68132b8c6b8ca79c2a.png

而jint是:

e936dbc8a17d8fa2fbf7351392bb8120.png

接下来则进行遍历,将元素1个个打印出来:

d8acb5e383a817c38353da74afcaae3d.png

但是!!由于运行在手机Android系统里,而非电脑上,所以用printf打印是没法在logcat中看到的,所以这里需要引用NDK提供的日志库,先引入头文件:

260a921bd17a8f8d213a26b371852b0e.png

ec2e0bd4e724de50ac44b4feba3fc304.png

修改打印为:

206e4890dc22a0e8e0865cf77b052273.png

不过通常会将日志的输出定义一个宏,方便进行调用,如下:

58d001096513a06eade129ad40c392f6.png

好,实现好了之后,咱们来在Activity中来调用一下它:

dd245c0f325820d7f309d85748ead76d.png

然后编译运行,看logcat中的输出:

3c808be48233df025273e2f78efc5000.png

这样就用C++的方式来对Java中的数组进行了遍历了,再回到cpp的代码中,发现在遍历之后还调用了一个释放操作,如下:

ba122fb83dae4b28cabc53654bcf514e.png

其中第三个参数需要注意一下,先看一下该函数的声明:

192a2c2c2d8b473ecacc8c4caf0010d2.png

其值可以有0、1、2,其具体含义为:

0:刷新java数组,并释放c/c++数组。

1:JNI_COMMIT,只刷新java数组。

2:JNI_ABORT,只释放c/c++数组。

其中看下官方的注释:

356ccc8ce922c3b18f617d12a2ac66c0.png

下面咱们来试一下这个参数的作用,先在Activity中将int数组的元素打印出来以便观察:

20823ca5bcb9c036db61dca1e8971c4d.png

先用2来试一下:

aacc4a66830dc42b315a7ee20cc4ec08.png

下面运行看一下:

71d4c205eb42d311714bb245ea01fb17.png

那如果改为0或1呢?

b361ea572967de165d7405d9f2cbcd32.png

编译运行:

e16a29c5b319d3d8fc6073010fae9947.png

可能是跟平台有关,反正实验在mac上没看出啥区别,先记着这个参数的区别吧。

下面来看一下细节,就是关于JNIEnv的参数,在C和C++的定义是不一样的,这样也就决定了在C和C++编写JNI函数时也会存在差别,先来看一下具体的定义:

b0fae740d4ed62e7a47514fa50840a95.png

在C++是如上定义的,咱们进一步跟踪:

3261039f5349b9618ab0225cecb638bc.png

它是一个结构体,所以我们在编写JNI的实现时,就可以直接这样写:

b8c8e27c852e23b7e16900a45d16053d.png

然后咱们再看一下C的定义:

ea162c11a1bf6d90b945a1e5e23fdddf.png

此时该参数就变成了指针的指针了,如下:

ef44be74d9b69325d9550f6d06a19190.png

所以此时在JNI实现时就得这样写了:

ca4e387c021618661f8d3581e1d6ec5b.png

另外对于Android Studio编写的JNI那生成的.so的动态库是在哪里呢?平常我们项目中用到native一般都会引入so文件嘛,其实在这里:

2a020beb48c508ef44f57c04b6d14614.png

接下来我们再对Java传过来的字符串数组进行遍历,如下:

73c5c4d14518d3025292af94139a3b49.png

然后咱们再将jstring打印出来,但是可惜不能直接打印,需要进行如下转换才行:

22871c4da0d4d588f1e9512dd82735f6.png

编译运行:

00f973a66888b124a6610d3ec4fd4ba7.png

C/C++反射Java:

新定义一个native方法,传递一个我们自定义的类,如下:

7867c70547c3d7295478db0259358639.png

2c14c1ae1a28883533f38d6ebfb5bd16.png

然后还是通过快捷键来生成出具体的实现函数,如下:

457f5f4bc334df0cfbb2fc37e615fe18.png

0b3fbd2bdb887a2f6a773f52f01adf44.png

反射方法:

接下来咱们来反射一下Java方法,首先得获取Java对象的Class对象才行,如下:

c5223489d81a7c25378567f8f4451095.png

接着找到要调用的方法,具体如下:

182882b98ee1c47795c6c9092aaf37bd.png

关于第三个参数如何传,其实如果研究过jvm字节码相关的信息一下就秒懂,这里涉及到数据类型在字节码的表示,基本数据类型的签名采用一系列大写字母来表示, 具体如下表所示:

Java类型签名

boolean

Z

short

S

float

F

byte

B

int

I

double

D

char

C

long

J

void

V

引用类型

L + 全限定名 + ;

数组

[+类型签名

所以第三个参数如下:

691ed257607917e896f2305e98912e74.png

如果说硬是不知道怎么写第三个的方法描述符,那可以用javap命令得到,如下:

1e1e312597bfd2aee64c74cd55bea806.png

下面具体来调用一下该方法:

ae2485f909be1acec826953fa2b8bcea.png

编译运行:

a29556aed36d5a5c851a3cb20dfccad9.png

同样的咱们还可以调用对象的setI()方法来改变对象里面的值,如下:

8b3b518b0dfc7a1ab5df437160ce87de.png

咱们在setI()方法中打一段日志用来观察:

e01cf33c36e2b44c9995bcc65dc2fc56.png

编译运行:

5b07f39dda2df21563277ef7fc709fcc.png

接下来咱们调用一下静态方法试试,首先在Bean中增加一个静态方法:

dd2b2d8778d5b1c03f1d219a1421166c.png

然后类似的做法在c++当中调用它:

06907675680a912b28a3c5ffba940309.png

编译运行:

4b8bb1bfa7821980bcab33affa982355.png

而如果方法中是自定义的类那如何调用呢,咱们新建一个类:

638401981e3cfe7b30b85e4c6a9e8662.png

然后再到Bean中再声明一个静态方法,参数传这个Bean2类,如下:

05f8b0350d7c7a4327e3cb513c4d5427.png

然后咱们在cpp中来调用它,这个就稍麻烦一些,如下:

c38b78780355a198b9d59e90f1dfba30.png

然后再用构造方法通过反射去生成对象:

f8ade7a90c5fc2821d65fc1b403a08ac.png

运行一下:

98a52e6321c5a0904253d3cee0bdff2d.png

反射属性:

也能够对类中的属性进行反射调用,用法基本跟反射方法的调用差不多,比如我们想反射Bean类中的i成员属性,如下:

ff41edc0ffce7c43e1740da5fc1d85e6.png

编译运行:

3456f7fe4d723f0a10f32ad30931c389.png

另外注意一个细节,就是在我们使用一个引用时在最后都进行了delete,如下:

4be88b353d6798404788dda87ef294cb.png

这里就涉及到了JNI引用相关的知识点了,所以下面来了解一下它。

JNI引用:

在 JNI 规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。

局部引用:

大多数JNI函数会创建局部引用。NewObject/FindClass/NewStringUTF 等等都是局部引用。局部引用只有在创建它的本地方法返回前有效,本地方法返回后,局部引用会被自动释放。因此无法跨线程、跨方法使用。

释放一个局部引用有两种方式:

1、本地方法执行完毕后VM自动释放;

2、通过DeleteLocalRef手动释放;

VM会自动释放局部引用,为什么还需要手动释放呢?

因为局部引用会阻止它所引用的对象被GC回收。如图所示:

c8947e4205f24f371d4619fa4f81e336.png

拿我们写的程序来说:

4b0921c1c9c5f6f5b30cffc78c5ec1b1.png

全局引用:

全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效 。由 NewGlobalRef 函数创建,下面来先来看一下如下程序:

77f877f65304bbc8c078fe6ff8fcdc33.png

然后Bean2中增加一个get方法:

5bbc9f7d81d4ba7da2fbe1a3c644bf85.png

然后在c++中去生成Bean2对像,假如针对调多次的情况想这样写:

9a36b9ddc606f174c5b5bb774736c9a8.png

咱们此时运行肯定是木啥问题的,但是!!如果native方法调用两次呢?

d2b607dfec367e7a9cdf558b70c041a7.png

1e9d83f64d67fe9aa07b86beea11f0fb.png

这是为啥呢?这是因为不能在本地方法中把局部引用存储在全局变量中缓存起来供下一次调用时使用。第二次执行时,其实bean2Class依然有值,但是其指向的地址数据已经被释放,也就是bean2Class成了悬空指针,所以第二次再用它时肯定就抛异常了,那如果想达到这样的效果就得用JNI提供的全局引用了,具体如下:

59ff1c47aa6d518a8ac670b9511cfbc2.png

如果全局引用不再使用了,可以手动用如下进行释放:

83bf5cae8b000661856be93f058c5fc2.png

弱引用:

与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象【其实跟Java的WeakReference差不多】 。

在对Class进行弱引用是非常合适(FindClass),因为Class一般直到程序进程结束才会卸载。

在使用弱引用时,必须先检查缓存过的弱引用是指向活动的对象,还是指向一个已经被GC的对象

用代码进行说明,假如想把其中的一个局部引用变为全局变量供其它程序使用,此时就可以使用弱引用,如下:

1e54fd61b18aa70f94d7efa4fac3d110.png

当然弱引用是不会阻止GC回收的,所以当我们使用弱引用的对像可能被回收,所以使用时需要判断弱引用的对像是否被回收了,所以代码可以这样改成:

a6bb6b496029705033360d1362e42214.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值