Jni(java native interface)是一种技术,它让java调用其他语言的代码,比如C/C++的代码.在SUN的官方网站上可以下载到相关的文档,看文档总是比较好的,给出链接先:
JVM TOOL DOC: http://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html JNI DOC: http://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html |
基本原理是将native的代码编译成动态库,在java中加载此动态库,java使用jni调用动态库中的函数,动态库中的函数也可以通过jni调用到java中的函数.
那么要实现这样的功能,需要解决:
1.java中怎样加载动态库 2.Java调用动态库的函数和动态库调用java函数问题 3.参数传递和返回值问题 4.C/C++运行的线程环境问题 5.异常的抛出与捕获问题 6.垃圾回收问题 |
1.java中怎样加载动态库
Java中加载动态库是调用函数:
System.loadLibrary("jnitest");
这个函数会在项目目录下查找动态库libjnitest.so(linux系统)或jnitest.dll(windows系统),开发android应用程序时,会在项目目录/libs下面查找.
加载时机是在调用库中的函数之前,通常的做法是在类的static域中加载,例如:
class JavaTestClass{ static{ System.loadLibrary("jnitest"); } //other method... }; |
2.Java调用动态库的函数和动态库调用java函数问题
在调用之前,需要完成java函数到动态库函数的一对一的映射才行.
2.1 动态库函数映射到java中
通过这种映射之后,java可以调用到动态库的函数.JVM提供了两种映射方法,静态映射和动态映射.无论静态映射还是动态映射,在java中函数声明都是一样的:
private native void jni_api();
2.1.1静态映射方法:
通常不用这种方法,因为它不好使,不过还是了解一下吧.在定义好java中的函数原型后,就可以使用工具javah通过类文件生成相应的C/C++头文件了.
javah -o output.h packetname.classname
将packetname.classname文件中的native函数原型转换成C/C++中的函数原型,结果保存在output.h中.例如:
javah -o jnitest.h com.test.JniTest
注意,执行的目录是在包的目录的上一层,不然可能找不到类文件.有了这个头文件,生活就变得容易了,C/C++代码只要包含这个头文件,然后实现函数就OK了.
这个背后的机制其实比较简单,java类中调用native标志的函数时,JVM会在一个映射表中查找映射函数,如果没有函数就按照一定的规则变换此函数原型,然后跟据变换后的原型调用动态中的函数.个人觉得这个方法比较麻烦,主要是函数名比较丑...,简单略过吧,我们主要使用动态映射方法.
2.1.2动态映射方法
上面有提到JVM会查找一个映射表...,我们可以将java中的函数原型和动态库中的函数原型通通注册到那个表中,JVM不就可以顺利找到java的另一半了吗?JVM确实提供了这样的功能.准确地说,从JVM提供了一些操作函数,同时JVM提供一个线程相关的结构体指针估且称为env,env包含了那组操作函数并且提供了包装输出了接口,把env输出的接口称为jni api,我们就是通过这些jni api和JVM打交道的,当然有时候也需要使用JVM本身提供的函数.
jni.h中定义了JNI API,来看一下和方法注册相关的:
typedef struct {
char *name; //函数在java中的名字
char *signature;//函数参数和返回值签名
void *fnPtr; //native代码中的函数指针
} JNINativeMethod;
//注册到映射表
jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,jint nMethods);
jint UnregisterNatives(jclass clazz);//取消注册
clazz是java中声明的native函数所在的类的类型对像,注意不是类的实例,methods是映射结构体指针,nMethods是映射结构体的个数.通常的做法是先定义一个JNINativeMethod类型的数组并初始化,然后使用RegisterNatives一次性注册.
看起来非常简单,但是有个问题,jclass怎么来的呢,还有那个签名怎么回事?先来看jclass:
jclass FindClass(const char *name);
参数name是java中类的名字,是全名,包含包的名字.
对于FindClass和RegisterNatives等许多的jni函数,你会看到别人的代码都是这么用的:
在C里面: (*env)->FindClass(env, "java/lang/String"); 在C++中: env->FindClass("java/lang/String"); 注意,所有的jni函数都是这么用的 |
签名是为了确定java中函数的参数和返回值,毕境上面只给出了name,怎么得到签名呢,一个是自己手动去写,这个需要熟悉类型到签名的转换,另一个就是使用工具javap,来看个例子:
javap -s com.test.JniTest public void play(java.lang.String); Signature: (Ljava/lang/String;)V |
很简单吧,不需要去记那么多规则,好吧,那么动态注册映射的时机呢,记得java函数中是在static中加载了动态库,那动态库可以在哪加载呢?
/* Defined by native libraries. */
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved);
JNIEXPORT void JNICALL
JNI_OnUnload(JavaVM *vm, void *reserved);
没错,就是他们,一个在加载后调用,一个在卸载后调用,动态库跟据自己的需要实现这两个函数.在加载动态库后,JVM会去查找动态库有没有提供JNI_OnLoad函数,如果没有JVM会抱怨一声然后正常加载,如果有就会调用它.参数vm是JVM的指针,可以从中获得env,从而干许多事.不管干了什么,这个函数最后要返回JNI的版本值,比如:
#define JNI_VERSION_1_1 0x00010001
#define JNI_VERSION_1_2 0x00010002
#define JNI_VERSION_1_4 0x00010004
#define JNI_VERSION_1_6 0x00010006
不返回的话,也没什么问题,只是JVM会报个错,然后整个进程都不好了.
2.2 java中的函数映射到动态库中
通过这种映射之后,动态库可以调用到java的函数.这个没有什么静态映射的方法了,是通过查找到java函数的句柄,然后使用jni api来使这个函数执行.查找句柄的jni api:
jmethodID GetMethodID(jclass clazz, const char *name,const char *sig);
其中clazz是通过FindClass返回的,name是java中函数的名字,sig是java函数的签名.找到这个句柄之后,就可以调用jni api来执行这个函数了,env根据返回值的不同提供了许多函数,比如:
jobject CallObjectMethod(jobject obj, jmethodID methodID, ...);
jboolean CallBooleanMethod(jobject obj,jmethodID methodID, ...);
void CallVoidMethod(jobject obj, jmethodID methodID, ...);
...
除了这些之外,还因为参数提供方法不一样分成了V和A版本:
void CallVoidMethodV(jobject obj, jmethodID methodID,va_list args);
void CallVoidMethodA(jobject obj, jmethodID methodID,const jvalue * args);
动态库除了可以访问java函数之外,还可以访问成员变量,同样是先取得句柄,然后调用jni api执行:
jfieldID GetFieldID(jclass clazz, const char *name,const char *sig);
jobject GetObjectField(jobject obj, jfieldID fieldID);
void SetObjectField(jobject obj, jfieldID fieldID, jobject val);
....
很多动态库保存全局结构体的方法是,在java中定义一个int的成员变量,然后在native代码中申请到空间后,写入到此成员变量,这样当java调用其他的函数时,在函数中就可以通过env获取到全局结构体的指针了.
3.参数传递和返回值问题
看到上面的jnit,jobject是不是觉得头晕,这得来介绍下java中的类型和jni中的类型,以及C/C++的类型:
java |
wide(byte) |
jni |
wide(byte) |
C/C++ |
byte |
1 |
jbyte |
1 |
char |
int |
4 |
jint |
4 |
int |
long |
8 |
jlong |
8 |
long long |
char |
2 |
jchar |
2 |
unsigned short |
boolean |
1 |
jboolean |
1 |
unsigned char |
short |
2 |
jshort |
2 |
short |
float |
4 |
jfloat |
4 |
float |
double |
8 |
jdouble |
8 |
double |
这是基本类型的对应表,这是啥意思呢,其实就是java中一个类型,比如char到了jni api时会转换成jchar,而jchar在C/C++中其实是unsigned short类型.其中黄色标记的类型长度是操作系统依赖的,这里写的都是linux.
除了基本类型的转换之外,还有数组和对像的类型.
java |
jni |
C/C++ |
object |
jobject |
类的指针 |
java.lang.Class |
jclass |
|
java.lang.Throwable |
jthrowable |
|
java.lang.String |
jstring |
|
boolean[] |
jbooleanArray |
|
byte[] |
jbyteArray |
|
char[] |
jcharArray |
|
short[] |
jshortArray |
|
int[] |
jintArray |
|
long[] |
jlongArray |
|
float[] |
jfloatArray |
|
double[] |
jdoubleArray |
|
object[] |
jobjectArray |
对于这种非基本类型的类型,在C/C++中访问的时候需要依靠env的帮助,跟据类型不同,这些jni api也不一样:
// 创建一个jbyteArray
jbyteArray NewByteArray(jsize len)
//从jbyteArray获取jbyte数组指针
jbyte * GetByteArrayElements(jbyteArray array, jboolean *isCopy);
//释放数组空间
void ReleaseByteArrayElements(jbyteArray array,jbyte *elems,jint mode);
......
参数isCopy用来指示在创建jbyte数组的时候是否进行了复制,如果进行了复制,*isCopy=JNI_TRUE,如果没有进行复制返回的是jbyteArray本身的数据地址那么*isCopy=JNI_FALSE.可以传入0作为参数,忽略其返回值.参数mode指示这个函数的操作方式:
Primitive Array Release Modes |
|
mode |
actions |
0 |
copy back the content and free the elems buffer |
JNI_COMMIT |
copy back the content but do not free the elems buffer |
JNI_ABORT |
free the buffer without copying back the possible changes |
4.C/C++运行的线程环境问题
java中可以有许多线程,当在这些线程中调用到动态库的函数时,其环境信息也会传递下来,JVM提供了一个结构体env来保存这些信息供动态库使用.怎么获得这个结构体呢,普通的被调用函数不用说了,参数中就有env结构体指针.要自己获取env结构体的地方有两个,一个是在JNI_Onload中,一个是在动态库自己创建的线程中.
在JNI_Onload中,可以通过JVM的接口获取:
jint GetEnv(JavaVM *vm, void **env, jint version);
在自已创建的函数中:
jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args);
jint DetachCurrentThread(JavaVM *vm);
或许有人会比较奇怪,为什么在自创线程中不能调用GetEnv得到env呢,其实是因为所有的线程都必须先AttachCurrentThread之后才能调用GetEnv,而java中创建的线程都调用了AttachCurrentThread,这个应该是在API中调用好了的.
java 创建一个JVM进程,JVM创建若干线程,所以JVM和线程无关,所有线程中共享一个JVM,我们可以保存起来使用,而env则和线程有关,必须AttachCurrentThread之后才能使用,用完后也必须释放掉.
5.异常的抛出与捕获问题
C/C++可以抛出异常给java代码:
jint Throw(jthrowable obj);
jint ThrowNew(jclass clazz,const char *message);//clazz应该是FindClass("java/lang/Throwable")返回的,message是给异常携带的错误信息.
C/C++可以捕获java抛出的异常:
jthrowable ExceptionOccurred(JNIEnv *env);//如果没有异常返回NULL,否则返回异常.
void ExceptionDescribe(JNIEnv *env);//打印当前异常信息
void ExceptionClear(JNIEnv *env);//清除当前异常信息
6.垃圾回收问题
JVM会不定时回收"垃圾",所谓的"垃圾"是指引用为0的对像,在java中每次赋值后都会增加左值的引用减少右值的引用,但是在C/C++中不会,所以这会引起问题,试想,java将一个对像传递给了native层代码,然后这个对像在java中引用减少为0了,那么它就会当成垃圾回收掉,这么可怕的事情怎么避免呢,当然是在native代码中也增加一次引用了.
jni中一共提供了三种引用类型,全局引用GlobalRef,本地引用LocalRef,弱全局引用WeadGlobalRef.
全局引用只能手动释放,不释放其对像会永久存在,除非进程被清理,它的创建与释放:
jobject NewGlobalRef(jobject obj);
void DeleteGlobalRef(jobject globalRef);
本地引用包括传为参数传进来的jobject和在函数中定义的jobject,本地引用会在函数返回时自动释放,也可以自己手动释放:
jobject NewLocalRef(JNIEnv *env, jobject ref);
void DeleteLocalRef(jobject localRef);
弱全局引用指向的对像可能被垃圾回收:
jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);
IsSameObject(jweak,NULL);//判断弱全局引用指向的对像是否被回收
注意一点,垃圾回收的时间没法确定,如果想快速释放一个对像,还是需要调用java层进行主动释放空间.
7.一个简单的例子:
com.test.JniTest.java:
package com.test; public class JniTest { static{ System.loadLibrary("jnitest"); } public native static void jni_api(); public static void main(String args[]){ try{ jni_api(); }catch(Throwable e){ System.out.println("------------------"); System.out.println(e.getMessage()); } } } |
jni/jnitest.h(使用命令javah -o jnitest.h com.test.JniTest生成
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_test_JniTest */
#ifndef _Included_com_test_JniTest #define _Included_com_test_JniTest #ifdef __cplusplus extern "C" { #endif /* * Class: com_test_JniTest * Method: jni_api * Signature: ()V */ JNIEXPORT void JNICALL Java_com_test_JniTest_jni_1api (JNIEnv *, jobject);
#ifdef __cplusplus } #endif #endif |
jni/jnitest.cpp:
#include <jni.h> #include "jnitest.h" #include <stdio.h>
JNIEXPORT void JNICALL Java_com_test_JniTest_jni_1api (JNIEnv *env, jobject thiz){ printf("----jni test---\n"); jclass c=env->FindClass("java/lang/Throwable"); printf("----find class 0x%x---\n",c); if(c) env->ThrowNew(c,"hello,this is exception"); else printf("can not find class!\n"); }
|
编译生成动态库:
gcc -shared -o ../libjnitest.so jnitest.cpp -I/usr/local/jdk1.7.0_45/include -I/usr/local/jdk1.7.0_45/include/linux
测试运行:
------------------
hello,this is exception
----jni test---
----find class 0x8f0821a8---