Floating Cat

JNI开发之RegisterNatives

字数统计: 2k阅读时长: 8 min
2016/12/20 Share

JNI开发流程相对比较固定,一般需要经过以下几步:

  1. 定义Native方法
  2. 生成.h头文件
  3. 编写C/C++文件
  4. 生成本地链接库

这里简单的写一个例子来演示上述流程.

JNI开发流程

定义Native方法

为了说明流程,我们直接在根目录中创建Hello.java:

1
2
3
4
5
6
7
8
9
public class Hello {
public native void say();

public static void main(String[] args) {
System.loadLibrary("hi");
Hello hello = new Hello();
hello.say();
}
}

需要注意如果链接库中有多个native方法,只需要一次System.loadLibrary().链接库在不同的平台中后缀不同:

  • window以.dll结尾

  • macOS以.jnilib结尾

  • linux中以.so结尾

    不要弄错,否则会导致UnsatisfiedLinkError.

javah生成.h头文件

Java中提供javah命令用于生成.h头文件.这里我们执行javah Hello命令即可生成对应的Hello.h.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Hello */

#ifndef _Included_Hello
#define _Included_Hello
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Hello
* Method: say
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_Hello_say (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

.h头文件名遵循格式:Package_ClassName,由于此处我们未创建包结构,因此Hello.java生成的.h文件名中省略了Package部分,即Hello.h

.h头文件方法名遵循格式Java_{Package_ClassName}_{FunctionName}(JNI arguments)比如JNIEXPORT void JNICALL Java_Hello_say (JNIEnv *, jobject),

jni.h是jdk中C语言库的头文件,在编译.c的时候需要指定jni.h的所在位置,否则会编译失败,在macOS中,其所在位置:System/Library/Frameworks/JavaVM.framework/Headers.

编写C/C++文件

有了.h头文件后,就可以为真正编写对应实现,这里创建了Hello.cpp,在方法输出Hello world.

1
2
3
4
5
6
7
8
#include "Hello.h"
#include <iostream>

/* 方法名需要和.h中声明的保持一致,不要弄错,否则会UnsatisfiedLinkError */
JNIEXPORT void JNICALL Java_Hello_say
(JNIEnv *, jobject){
std::cout << "Hello world!" << std::endl;
}

生成链接库

完成C/C++文件编写以后,通过以下命令将Hello.cpp编译成动态链接库.

1
g++ -fPIC -shared -o libhi.jnilib Hello.cpp -I/System/Library/Frameworks/JavaVM.framework/Headers
  • -fPIC : 要求编译器生成与位置无关的代码,否则会导致链接库在内存中无法被正确加载
  • -shared : 要求编译器生成共享链接库.使用该选项时必须指定-fPIC选项
  • -o : 该选项后需要指定动态链接库名字,需要保证其后缀名和运行平台对应,比如linux下是.so,mac下是.jnilib
  • -I : 该选项后需要指定jni.h文件所在的路径

image-20180829122221898

接下来通过命令:java Hello运行主程序:

1
Hello world!

这里为了省事,我直接用g++来编译.

JNI注意事项

在JNI开发过程中我们经常会遇到类问题:一是.h文件创建失败,另一类是调用过程,经常会遇到UnsatisfiedLinkError,

.h创建失败

在有些情况,通过javah创建.h文件时,可能会遇到错误:Error: Could not find class file for 'XXX'.产生该问题的原因一般由于javah中路径指定有问题.在上述例子中由于我们Hello.java不存在包结构,因此在根目录下执行’javah Hello’会正常生成.h文件.但如果项目结构如下所示:

image-20180829133113486

此时需要先进入src目录下,然后在该目录下执行如下命令:javah -d jni com.lionoggo.Hello即可,此处-d参数用来指定头文件存放路径.

image-20180829133317779

UnsatisfiedLinkError

该错误一般由于链接库路径不对或者链接库自身问题导致.

  1. 链接库路径问题

    如果链接库和主程序不在同一目录内,在执行时需要指定链接库路径,如下目录结构:

    image-20180829142537772

此时在执行主程序时需要指定路径:java -Djava.library.path=./lib Hello

  1. 链接库本身

    此处需要注意在macOS中动态链接库的后缀为jnilib,因此这里不要写成’g++ -fPIC -shared -o libhi.so …’,否则会导执行Java程序抛出以下错误:

1
2
3
4
5
Exception in thread "main" java.lang.UnsatisfiedLinkError: no hi in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
at java.lang.Runtime.loadLibrary0(Runtime.java:870)
at java.lang.System.loadLibrary(System.java:1122)
at Hello.main(Hello.java:5)

RegisterNatives解耦Native方法

回过头来看上述Native的方法名:

1
JNIEXPORT void JNICALL Java_Hello_say (JNIEnv *, jobject);

在没有包名的前提下,该方法看起来没什么问题,但是现在假设我们存在包名结构:com.lionoggo.ireader,那这个方法名就会变得很长:

1
JNIEXPORT void JNICALL Java_com_lionoggo_ireader_Hello_say (JNIEnv *, jobject);

除此之外,每个方法都要带固定的JNIEXPORT void JNICALL,这就导致无法将Native方法名修改为更简洁的方式.JNI中提供了RegisterNatives()为Java中的Native方法动态绑定某个具体Native的实现方法.

JNI_OnLoad/JNI_OnUnload

JNI组件被成功加载和卸载时,回回调响应的函数.当JVM执行执行System.loadLibrary()时会调用JNI组件的JNI_ONLoad(JavaVm *vm,void *reserved),当VM释放JNI组件时会调用JNI_OnUnload().

1
2
3
/* Defined by native libraries. */
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved);

要在JNI组件被加载时动态绑定Native方法,那么JNI_OnLoad()就是个非常好的时机,这里结合刚才提到的RegisterNatives来为Java中Native方法绑定具体的实现.

RegisterNatives

1
2
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,
jint nMethods)

该方法是JNI环境提供的用来注册Native方法的方法.期三个参数含义如下:

  • clazz: Java Native类
  • methods: 用来注册的Native方法数组
  • nMethods: 用来注册的Native方法数目

JNINativeMethod

JNINativeMethod是jni.h中定义的结构体,用来描述Native方法,macOS中可以在路径System/Library/Frameworks/JavaVM.framework/Headers/jni.h找到:

1
2
3
4
5
6
7
8
9
10
/*
* used in RegisterNatives to describe native method name, signature,
* and function pointer.
*/

typedef struct {
char *name; // native方法名
char *signature; // native方法签名
void *fnPtr; // native函数指针
} JNINativeMethod;

具体实现

有了JNI_OnLoad和RegisterNatives后,我们就可以来实现Native方法的动态注册了,这里仍然之前的Hello.java作为演示,其过程相对固定,主要分为三步:

  1. 根据需要实现相应的Native方法:

    1
    2
    3
    void sayHello(){
    std::cout << "Hello world!" << std::endl;
    }
  2. 定义需要被绑定的本地方法.

    1
    2
    3
    4
    5
    static JNINativeMethod methods[] = {
    //java类native方法名|方法签名|Native实现方法名
    {"say","()V",(void*)sayHello},

    };
  3. 实现JNI_OnLoad方法,并使用RegisterNatives绑定java和native方法对应关系:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
    {
    JNIEnv* env;

    // 1.获取JNI环境对象
    if (JNI_OK != vm->GetEnv(reinterpret_cast<void**> (&env),JNI_VERSION_1_4)) {
    //LOGW("JNI_OnLoad could not get JNI env");
    return JNI_ERR;
    }

    // 2.获取Java Native类
    g_jvm = vm;
    jclass clazz = env->FindClass(classPathName); //获取Java NativeLib类
    if (clazz == NULL) {
    return JNI_ERR;
    }
    // 3.调用RegisterNatives注册Native方法
    if (env->RegisterNatives(clazz, methods, sizeof(methods)/sizeof((methods)[0])) < 0) {
    return JNI_ERR;
    }
    return JNI_VERSION_1_4;
    }

经过以上三部,就可以实现动态绑定Native方法了.HelloRegister.cpp的完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <string.h>
#include <jni.h>
#include <iostream>

JavaVM* g_jvm;

// Method define start
void sayHello(){
std::cout << "Hello world!" << std::endl;
}
// Method define end

static const char * classPathName = "com/lionoggo/Hello";


static JNINativeMethod methods[] = {
//java类native方法名|方法签名|Native实现方法名
{"say","()V",(void*)sayHello},

};

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv* env;
// 1.获取JNI环境对象
if (JNI_OK != vm->GetEnv(reinterpret_cast<void**> (&env),JNI_VERSION_1_4)) {
//LOGW("JNI_OnLoad could not get JNI env");
return JNI_ERR;
}
g_jvm = vm;
// 2.获取Java Native类
jclass clazz = env->FindClass(classPathName);
if (clazz == NULL) {
return JNI_ERR;
}
// 3.调用RegisterNatives注册Native方法
if (env->RegisterNatives(clazz, methods, sizeof(methods)/sizeof((methods)[0])) < 0) {
return JNI_ERR;
}
return JNI_VERSION_1_4;
}

将其打包成本地链接库libhi.jnilib,然后像之前一样执行Hello.java即可.

总结

RegisterNatives除了能解决原来JNI中方法遵循特定方法命名规则外,还具备以下两个优点:

  • VM查找Native效率的提高

    在调用Native方法时,VM需要多次在本地链接库中进行查找,多次调用就会产生多次查找.而通过RegisterNatives注册后,VM的查找次数会减少.

  • 在执行期间进行Native方法的替换.

    在methods通过函数指针来绑定方法,这意味着我们可以在运行期间多次调用RegisterNatives来实现Native方法的替换.

参考

Java Native Interface

CATALOG
  1. 1. JNI开发流程
    1. 1.1. 定义Native方法
    2. 1.2. javah生成.h头文件
    3. 1.3. 编写C/C++文件
    4. 1.4. 生成链接库
  2. 2. JNI注意事项
    1. 2.1. .h创建失败
    2. 2.2. UnsatisfiedLinkError
  3. 3. RegisterNatives解耦Native方法
    1. 3.1. JNI_OnLoad/JNI_OnUnload
    2. 3.2. RegisterNatives
    3. 3.3. JNINativeMethod
    4. 3.4. 具体实现
    5. 3.5. 总结
  4. 4. 参考