在 不使用IDE做一次JNI开发 一文中,我们使用了"静态注册"的方法建立 Java 世界 native 方法和 Native 世界函数的一一对应关系。
"静态注册"方法确实帮我们省了很多事情,但是也有相应的缺点
Java_包名_类名_方法名
,例如 Java_com_bxll_jnidemo_Hello_helloFromJNI
。 有"静态注册",当然就有"动态注册",那么相比较而言,有哪些优缺点呢
虽然"动态注册"相比于"静态注册"有这么多好处,但是需要一定的学习成本,但是这也是非常值得的,那么我们就开始吧。
我们知道,在 Java
层通过 System.loadLibrary()
方法可以加载一个动态库,此时虚拟机就会调用 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
如何获取会在后面的例子中讲解。
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()
函数的使用就行。至于具体的细节,还需要大家跟着例子仔细体会。