在 不使用IDE做一次JNI开发 一文中,我们使用了"静态注册"的方法建立 Java 世界 native 方法和 Native 世界函数的一一对应关系。
"静态注册"方法确实帮我们省了很多事情,但是也有相应的缺点
- 首次调用 Java 的 native 方法,虚拟机会去搜寻对应的 Native 层的函数,这就有点影响执行效率了。如果搜索到了,就会建立映射关系,下次就不用再浪费时间去搜索了。
- Native 层的函数名字太长,名字的格式为
Java_包名_类名_方法名,例如Java_com_bxll_jnidemo_Hello_helloFromJNI。 - 太麻烦,影响工作效率(个人工作体验)。
有"静态注册",当然就有"动态注册",那么相比较而言,有哪些优缺点呢
- "动态注册"需要我们手动建立函数映射关系,虽然增加了代码量,但是可以提供运行效率。
- "动态注册"允许我们自定义函数名字。
- 相比于"静态注册",工作效率高。
虽然"动态注册"相比于"静态注册"有这么多好处,但是需要一定的学习成本,但是这也是非常值得的,那么我们就开始吧。
加载动态库
我们知道,在 Java 层通过 System.loadLibrary() 方法可以加载一个动态库,此时虚拟机就会调用JNI库中的 JNI_OnLoad() 函数(放在哪个文件无所谓),函数原型如下
#include <jni.h>
jint JNI_OnLoad(JavaVM* vm, void* reserved);
参数介绍
vm:JavaVM指针,我们在 连接Java世界的JavaVM和JNIEnv 一文中介绍过。reserved: 类型为void *,这个参数是为了保留位置,以供将来使用。
返回值代表被动态库需要的JNI版本,当然,如果虚拟机无法识别这个返回的版本,那么动态库也加载不了。
目前已有的返回值有四个,分别为 JNI_VERSION_1_1, JNI_VERSION_1_2, JNI_VERSION_1_4, JNI_VERSION_1_6。
如果动态库没有提供 JNI_OnLoad() 函数,虚拟机会假设动态库只需要 JNI_VERSION_1_1 版本即可,然而这个版本太旧,很多新的函数都没有,因此我们最好在动态库中提供这个函数,并返回比较新的版本,例如 JNI_VERSION_1_6。
注册函数
JNI_OnLoad() 函数经常会用来做一些初始化操作,"动态注册"就是在这里进行的。
"动态注册"可以通过调用_JNIEnv结构体的 RegisterNatives() 函数
struct _JNIEnv {
const struct JNINativeInterface* functions;
#if defined(__cplusplus)
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,
jint nMethods)
{ return functions->RegisterNatives(this, clazz, methods, nMethods); }
#endif /*__cplusplus*/
}
实际使用的函数原型如下
jint RegisterNatives(JNIEnv *env, jclass clazz,
const JNINativeMethod *methods, jint nMethods);
参数解释
env:JNIEnv指针,我们在 连接Java世界的JavaVM和JNIEnv 一文中介绍过。clazz: 代表 Java 的一个类。methos: 代表结构体JNINativeMethod数组。JNINativeMethod结构体定了Java层的native方法和底层的函数的映射关系。nMethods: 代表第三个参数methods所指向的数组的大小。
返回值
0代表成功,负值代表失败。
第二个参数
jcalss clazz如何获取会在后面的例子中讲解。
JNINativeMethod结构体
RegisterNatives() 函数最重要的部分就是 JNINativeMethod 这个结构体,我们看下这个结构体声明
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
name表示Java的native方法的名字。
signature表示方法的签名。
fnPtr是一个函数指针,指向JNI层的一个函数,也就是和Java层的native建立映射关系的函数。
那么这个三个参数如何指定呢?我首先教大家一个偷懒的方法,我们在不使用IDE做一次JNI开发使用javah命令生成过一个头文件,函数原型如下
/*
* Class: com_bxll_jnidemo_Hello
* Method: helloFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_bxll_jnidemo_Hello_helloFromJNI
(JNIEnv *, jclass);
JNINativeMethod 结构体中的 name 的值就对应注释的 method 的值,也就是 helloFromJNI。
JNINativeMethod 结构体中的 signature 的值就对应注释的 Signature 的值,也就是 ()Ljava/lang/String;。
JNINativeMethod 结构体中的 fnPtr 指针要指向哪个函数呢?我们可以实现一个函数,就使用这个原型,但是名字可以自己定义,并且记得去掉JNIEXPORT和JNICALL。
实现动态注册
有了前面的所有基础,那么我们就可以实现自己的动态注册功能了。
首先带有native方法的Java类如下
package com.bxll.jnidemo;
class Hello
{
native String helloFromJNI();
}
然后我们可以使用javah命令生成头文件来帮我们准确无误的实现动态注册,生成的头文件中函数原型如下
/*
* Class: com_bxll_jnidemo_Hello
* Method: helloFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_bxll_jnidemo_Hello_helloFromJNI
(JNIEnv *, jclass);
根据头文件的注释和函数原型,我们就可以实现如下的动态注册
#include <jni.h>
static jstring
native_helloFromJNI(JNIEnv *env, jobject thiz) {
const char *hello = "Hello from C++";
return env->NewStringUTF(hello);
}
const JNINativeMethod methods[] = {
{"helloFromJNI", "()Ljava/lang/String;", (void *) native_helloFromJNI}
};
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
int jniVersion = -1;
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) == JNI_OK) {
// 找到com.bxll.jnidemo.Hello类,只不过参数需要把点号替换为下划线
jclass clazz_hello = env->FindClass("com/bxll/jnidemo/Hello");
if (env->RegisterNatives(clazz_hello, methods,
sizeof(methods) / sizeof(methods[0])) == JNI_OK) {
jniVersion = JNI_VERSION_1_6;
}
}
return jniVersion;
}
必须要引入头文件
jni.h,这个头文件是JNI所必须的。
提升工作效率
我是一个Android开发者,在项目中经常会遇到在底层开发一个功能,然后需要通过JNI向上层提供接口,这个时候就需要快速的定义Java的native函数,以及实现"动态注册",如果每次都需要使用头文件来支持"动态注册",那么开发效率着实的低下。那么我们怎么才能快速的写出"动态注册"所需要的一切东西呢?那就需要对JNI类型以及签名非常熟悉,我会在下一篇文章中进行讲解,并且让大家看到如何最快的速度实现"动态注册"所需要的一切。
总结
"动态注册"功能还是比较简单的,只需要搞清楚JNI_OnLoad()和RegisterNatives()函数的使用就行。至于具体的细节,还需要大家跟着例子仔细体会。