JNI技术之动态注册
一.静态注册的缺点
1.C/C++实现的函数名,必须通过javah工具生成对应的C/C++的头文件。
2.初次调用native函数时,采用的是在动态库中寻找,如果native函数较多的话,效率比较低下的。
如果能在java层将所有的native函数直接将与C/C++实现的函数关联起来,那我们在调用C/C++实现的函数时,就不需要到动态库中寻找对应的函数接口。提供了运行效率。
二.动态注册
如何将java native函数和C/C++函数关联起来,Jni中有如下结构体。
/*
* used in RegisterNatives to describe native method name, signature,
* and function pointer.
*/
typedef struct {
char *name; //java层native函数名
char *signature;//签名信息,用字符串表示,是返回值和参数的结合
void *fnPtr;//记录JNI层函数的入口地址
} JNINativeMethod;
JNI层C/C++的代码:
#include <jni.h>
#include "Test.h"
#include <stdio.h>
int jni_sayHello(JNIEnv *env, jobject obj) {
printf("hello world!!!\n");
return 0;
}
static JNINativeMethod gMethods[] = {
{
"sayHello",
"()V",
(void *)jni_sayHello
},
};
在JNI层首先要填充JNINativeMethod类型的结构体,JAVA层可能有多个native函数,所以这里以结构体数组的形式呈现的,你可以添加所有JAVA层native函数和JNI层函数的一一对应关系。
关键是签名信息,是由java层native函数的返回值类型和参数类型组成的信息。原因是java函数支持函数重载。签名信息正好可以进行区分的。
JNI签名信息:
格式:
(参数1类型标识 参数2类型标识 …参数n类型标识)返回值类型标识
例如:JAVA层的native函数
void testFuction(int a, short b, char c, String str, HelloWorld obj);
它对应的JNI层签名信息如下:
(ISCLjava/lang/String;LHelloWorld;)V
其中括号内部是参数类型的标识,最右边是返回值类型标识的。
Java提供了一个javap的工具能帮助生成函数或变量的签名信息,它的用法如下:
javap -s -p xxx
其中xxx为编译后的class文件名,s表示输出内部数据类型的签名信息,p表示打印函数和成员的签名信息,默认只会打印public成员和函数的签名信息。
先用javac Test.java
再用javap -s -p Test
我们知道了JAVA层native函数和JNI层函数间的关系,还需要将它们之间的关系告知java虚拟机。
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
jint result = -1;
//获取JNIEnv环境
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4) != JNI_OK) {
printf("JavaVm fail to get JNIEnv\n");
return -1;
}
printf("JNI_OnLoad\n");
// 动态注册JNI函数
if(register_jni_function(env) < 0) {
printf("fail to register jni function");
return -1;
}
return JNI_VERSION_1_4;
}
java层在调用System.loadLibrary()加载动态库后,java虚拟机就会去该库中寻找JNI_OnLoad这个函数,如果有则会调用它,我们的动态注册也就是在这里完成的,所以如果想实现动态注册,就必须实现JNI_OnLoad这个函数。
jint: JNI层,整型的标示方法
JavaVM:用来描述java虚拟机的数据类型
JNIEnv: 用来描述Jni层代码运行的环境,其实这个结构体中包含了很多函数指针,通过这些函数指针我们可以操作JAVA层的对象,完成动态注册。
例如:创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等等。
JAVA层的native函数在JNI层实现的时候,总是会带两个参数,一个就是JNIEnv指针,另一个是object表示Java层的对象。
Test.java
class Test {
//通过"native"修饰成员函数接口,表示这个函数接口是由C/C++来实现的
public native void sayHello();
static {
System.out.println("start load static lib");
//通过System.loadLibrary()加载对应的动态库
System.loadLibrary("helloworld");
}
public static void main(String[] args) {
Test mObj = new Test();
mObj.sayHello();
}
}
Test.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Test */
#ifndef _Included_Test
#define _Included_Test
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Test
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_Test_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
helloworld.c
#include <jni.h>
#include "Test.h"
#include <stdio.h>
int jni_sayHello(JNIEnv *env, jobject obj) {
printf("hello world!!!\n");
return 0;
}
static JNINativeMethod gMethods[] = {
{
"sayHello",
"()V",
(void *)jni_sayHello
},
};
int register_jni_function(JNIEnv *env) {
jclass clazz;
//通过JNIEnv获取Test类
clazz = (*env)->FindClass(env, "Test");
if(clazz == NULL) {
return -1;
}
printf("Findclass\n");
//调用JNIEnv的RegisterNatives函数完成注册
if((*env)->RegisterNatives(env, clazz, gMethods, 1) < 0) {
return -1;
}
printf("RegisterNatives\n");
return 0;
}
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
jint result = -1;
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4) != JNI_OK) {
printf("JavaVm fail to get JNIEnv\n");
return -1;
}
printf("JNI_OnLoad\n");
if(register_jni_function(env) < 0) {
printf("fail to register jni function");
return -1;
}
return JNI_VERSION_1_4;
}
为啥要先知道类呢?
java是通过加载类来运行的,由于Java虚拟机中有多个类,每个类可能有native方法,所以在注册时候,就必须告诉java虚拟机,我们注册的是哪一类的native方法。
三.生成动态库,运行JAVA程序
1.生成动态库
gcc -fpic -shared -o libhelloworld.so helloworld.c -I /usr/lib/jvm/java-8-openjdk-amd64/include/ -I /usr/lib/jvm/java-8-openjdk-amd64/include/linux/
其中-I 指定jni.h文件所在的路径
2.运行java程序