在 不使用IDE做一次JNI开发 一文中,我们使用了"静态注册"的方法建立 Java 世界 native 方法和 Native 世界函数的一一对应关系。
"静态注册"方法确实帮我们省了很多事情,但是也有相应的缺点
- 首次调用 Java 的 native 方法,虚拟机会去搜寻对应的 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
如何获取会在后面的例子中讲解。
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()
函数的使用就行。至于具体的细节,还需要大家跟着例子仔细体会。