C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数

C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数

排版不佳建议点击查看原文

静态注册


在上篇中我们介绍了JNI基本内容,其中说到本地函数命名规则:返回值Java_包名_类名_本地方法名。按照此命名规则我们当然是可以创建对应的函数的,可是如果java本地方法数量过多,这时候就需要生成xxx.h头文件来完成函数的声明。

这其实就是JNI函数的注册问题,“注册”之意就是将java层的native方法和JNI层对应的实现函数关联映射起来。而JNI注册的方式其实有两种,上篇介绍的例子全部都是使用静态注册的方式,可以去回顾下。

为什么要动态注册


静态注册,其实就是根据函数名建立java与C函数的映射,其中严格要求JNI层的C函数命名必须遵循特定的格式。这种方式有几个弊端:

1.需要编译所有声明了native方法的java类,每个类生成的class文件都需要用javah命令生成一个头文件。

2.javah生成的JNI层的函数名特别长,容易出错,书写不便。

3.初次调用native方法时需要根据方法名称搜索对应JNI层的函数名称来建立映射关系,这样会影响运行效率。

有什么方法可以解决上面的弊端吗?java native方法是通过函数指针来和JNI层函数建立映射关系的,如果让native方法知道对应JNI层函数的函数指针,不就解决了吗,这就是动态注册法。

动态注册


我们还是通过一个小例子来实现动态注册把!

C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数_第1张图片

这里我们写了一个java类JNIDemo,代码很简单,这里我们全程在Liunx环境下编译运行。

C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数_第2张图片

上面的C代码如果看过上一篇文章的童鞋应该不算太陌生了,我们一行一行解析:

JNIEXPORT jint JNICALL

这里JNIEXPORT和JNICALL都是JNI的关键字,表示此函数是要被JNI调用的。而jint是以JNI为中介使JAVA的int类型与本地的int沟通的一种类型,我们可以视而不见,就当做int使用。

JNI_OnLoad(JavaVM *jvm, void *reserved)

当java层通过System.loadLibrary("native");加载完JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数,如果有它则调用它,而动态注册的工作就是这里完成的。

3.JavaVM *jvm

第一个参数JavaVM,它当然是一个结构体,我是通过查看jni.h文件的源码得知的(在ubuntu环境中安卓的是openjdk,jni.h的路径是/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/)

C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数_第3张图片

在源码中我们搜索JavaVM关键字:

C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数_第4张图片

发现JavaVM其实是JavaVM_的自定义类型,搜索JavaVM_:

C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数_第5张图片

我们发现JavaVM_其实是一个结构体,里面定义了许多函数,这里一会用到的函数是GetEnv它就是这里定义的,暂且记住。仔细发现所有的函数都是通过JNIInvokeInterface_ 这个结构体的指针functions调用JNIInvokeInterface_ 结构体中的函数实现的,我们搜索JNIInvokeInterface_ :

C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数_第6张图片

在JNIInvokeInterface_ 这个结构体中定义了5个函数指针,其中就有我们即将用到的GetEnv,这个函数需要传三个参数vm、penv、version,vm就是JavaVM这个结构体,penv接收JNIEnv这个结构体,需要另外声明,version就是JNI的版本号。

回到代码中你对JNI_OnLoad的JavaVM *jvm这个参数是不是有个初步了解啦。JavaVM 它是java虚拟机在JNI层的代表,我们知道全进程只有一个虚拟机,自然只有一个JavaVM对象,所以它可以被保存并且在任何地方使用都ok的。

那么JavaVM和JNIEnv有什么关系呢?(JNIEnv在上篇中有详解)


调用GetEnv这个函数可以得到本线程的JNIEnv结构体,JNIEnv是一个与线程相关的代表JNI环境的结构体,实际就是提供了一些JNI系统函数,这样就可以在后台线程回调java方法了。

JNIEnv是一个与线程相关的变量,也就是说每个线程中的JNIEnv并不通用,压根不是一个事物。你可能会问,我们在上篇java本地方法转换成C函数的时候JNIEnv对象不是已经在函数的形参中了吗?(如下图引用自上篇中的某个头文件)

C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数_第7张图片

没错,图上的JNIEnv是由虚拟机传进来的,我们直接使用函数中的这个JNIEnv没有任何问题。

假设一个情况:

Native层定义一个函数A,职责是回调Java函数。这个时候子线程a收到网络消息调用了函数A,函数A则需要通过JNIEnv提供的函数回调Java方法。那么JNIEnv在函数A中并不存在(它并不是与java本地方法映射的函数,形参中并没有JNIEnv)我们也不能保存另外线程的JNIEnv结构体,然后让给子线程a使用。这该如何是好?

回到主题,JNI_OnLoad函数的第一个参数JavaVM,它是虚拟机在JNI层的代表:

JNIEXPORT jint JNICALL

JNI_OnLoad(JavaVM *jvm, void *reserved)

全进程只有一个JavaVM对象,所以可以保存,并且在任何地方使用都没有问题。那么JavaVM和JNIEnv有什么关系呢?

调用JavaVM的GetEnv函数,就可以得到此线程的JNIEnv结构体,这样完美解决了线程a调用函数A回调java方法的问题了。

代码中我们通过GetEnv函数的返回值判断是否成功获取了JNIEnv对象:

然后通过FindClass函数反射得到java类的字节码对象(上篇有详细讲解),如果获取失败依然返回错误信息。

最后调用RegisterNatives注册函数:

C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数_第8张图片

有必要说下最后两个参数methods和1:

methods是一个结构体数组

C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数_第9张图片

既然java本地方法和JNI函数是壹壹对应的,那么是不是会有一个结构体来保存这种关系呢?肯定的!在JNI中用来记录这种对应关系的是一个叫JNINativeMethod的结构体,如图上代码所示(#if 0 , #endif中间的代码不参与编译,视为已注释)。methods数组的成员正是JNINativeMethod,也就是说有多少个java本地方法就可以有多少个与之对应的JNINativeMethod保存在methods数组中。

这里我们只有一个本地方法hello(),所以只需要定义一个JNINativeMethod:{"hello", "()V", (void *)c_hello},

1).“hello”是java本地方法的名称

2).“()V”就是方法签名(上篇有详解)

3).(void *)c_hello就是与java本地方法映射的C函数,这里我们取名为c_hello,打印了HelloWord。

2.回到RegisterNatives注册函数,最后一个参数是“1”,代表我们需要注册多少个函数,这里只有一个所以传“1”。

JNI_OnLoad函数最终需要返回版本号JNI_VERSION_1_4,别问我为啥,我也不太清楚,等你告诉我!

编译与总结


至此JNI动态注册的小例子就写完了,我们在Liunx环境编译下:

C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数_第10张图片

一共俩文件JNIDemo.java和native.c,首先编译C文件,这命令你可能蒙了,不是“gcc -o nativenative.c”吗?当然不是,我们需要生成动态链接库的,需要加上so库的名称以及你的jni.h这个头文件的路径,系统才能找到对应的函数。我这里编译命令是“gcc -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/ -fPIC -shared -o libnative.so native.c”。还没完!很可能你就报找不到jni_md.h文件的错误!,既然找不到那你就把jni_md.h拷贝到与jni.h同一个目录下就解决了!(jni_md.h我这里路径是/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/liunx)。

Java的编译太简单了,如上图。

编译完成后你以为直接java JNIDemo程序就能跑起来,结果是报错了,如下图:

C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数_第11张图片

异常信息提示找不到java.library.path,意思是找不到so库,因为我们少了一步:“export LD_LIBRARY_PATH=.”当前目录下搜索对应的动态链接库。

图上便是最终运行结果!

总结下,其实动态注册的工作全体围绕着RegisterNatives注册函数去实现的。

C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数_第12张图片

实现JNI_OnLoad函数可以完成很多初始化操作,其中也包括JNIEnv *env;有了env我们才能够调用注册函数,完成后面的每一个形参。更推荐使用动态注册方式实现JNI。

欢迎长按下图-识别图中二维码或者扫一扫,搜索微信公众号:黄君华。关注我的公众号:

C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数_第13张图片

如果你有不同意见或建议或者有好的技术文章想和大家分享欢迎投稿,可以把你的文章使用附件的形式发送到我的邮箱[email protected]

谢谢阅读!

你可能感兴趣的:(C、C++、Java? Java Native Interface(JNI)特辑--动态注册JNI函数)