JNI函数动态注册

3,096 阅读5分钟

不使用IDE做一次JNI开发 一文中,我们使用了"静态注册"的方法建立 Java 世界 native 方法和 Native 世界函数的一一对应关系。

"静态注册"方法确实帮我们省了很多事情,但是也有相应的缺点

  1. 首次调用 Java 的 native 方法,虚拟机会去搜寻对应的 Native 层的函数,这就有点影响执行效率了。如果搜索到了,就会建立映射关系,下次就不用再浪费时间去搜索了。
  2. Native 层的函数名字太长,名字的格式为 Java_包名_类名_方法名,例如 Java_com_bxll_jnidemo_Hello_helloFromJNI
  3. 太麻烦,影响工作效率(个人工作体验)。

有"静态注册",当然就有"动态注册",那么相比较而言,有哪些优缺点呢

  1. "动态注册"需要我们手动建立函数映射关系,虽然增加了代码量,但是可以提供运行效率。
  2. "动态注册"允许我们自定义函数名字。
  3. 相比于"静态注册",工作效率高。

虽然"动态注册"相比于"静态注册"有这么多好处,但是需要一定的学习成本,但是这也是非常值得的,那么我们就开始吧。

加载动态库

我们知道,在 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);

参数解释

  1. env: JNIEnv指针,我们在 连接Java世界的JavaVM和JNIEnv 一文中介绍过。
  2. clazz: 代表 Java 的一个类。
  3. methos: 代表结构体 JNINativeMethod 数组。JNINativeMethod结构体定了Java层的native方法和底层的函数的映射关系。
  4. nMethods: 代表第三个参数methods所指向的数组的大小。

返回值

0代表成功,负值代表失败。

第二个参数jcalss clazz如何获取会在后面的例子中讲解。

JNINativeMethod结构体

RegisterNatives() 函数最重要的部分就是 JNINativeMethod 这个结构体,我们看下这个结构体声明

typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;

name表示Javanative方法的名字。

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 指针要指向哪个函数呢?我们可以实现一个函数,就使用这个原型,但是名字可以自己定义,并且记得去掉JNIEXPORTJNICALL

实现动态注册

有了前面的所有基础,那么我们就可以实现自己的动态注册功能了。

首先带有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向上层提供接口,这个时候就需要快速的定义Javanative函数,以及实现"动态注册",如果每次都需要使用头文件来支持"动态注册",那么开发效率着实的低下。那么我们怎么才能快速的写出"动态注册"所需要的一切东西呢?那就需要对JNI类型以及签名非常熟悉,我会在下一篇文章中进行讲解,并且让大家看到如何最快的速度实现"动态注册"所需要的一切。

总结

"动态注册"功能还是比较简单的,只需要搞清楚JNI_OnLoad()RegisterNatives()函数的使用就行。至于具体的细节,还需要大家跟着例子仔细体会。