JNI(java native interface)主要用于java调用原生代码(java中间代码运行于虚拟机,虚拟机不具跨平台性,原生代码也是一样的,这个都知道的^_^)。所以JNI应该是在java的代码和native的库间存在映射关系,java代码调用native code时通过jvm查找到相应函数的地址执行,调用思路和dll、so类似,然后dll、so和jvm在同一个进程的不同地址上,平行的关系(个人看法)。
开发jni可以使用c/c++或其他的编译性语言。开发过程中有些jni的规定需要遵守,虽然jni不运行于虚拟机,但总是要和jvm打交道,每个厂商都会提供一套和jvm打交道的接口实现,基础接口都是一样的。其中主要是jni代码使用jvm资源的管理,不同语言数据类型的转换,怎么回调,线程调用jvm资源等。
下面逐个展开说明:
1.怎么定义JNI接口。
首先需要引用jni.h这个头文件,以后很多和jvm打交道的函数都在这个头文件中。
我在摸索JNI的写法时,最初使用javah生产头文件,先写好一个java的native接口文件,然后通过Javah生成jni头文件,生成头文件如下:
JNIEXPORT jint JNICALL Java_com_test_NativeAPI_Init
(JNIEnv *, jclass);
其中com.test是包名,NativeAPI是java的类名,Init是方法名。里面的JNIEnv, jclass(jobject)是每个Jni接口必须要用的参数,表示每次调用的上下文环境和调用者。
在开发过程中我们往往会加接口,每次都去javah生成时很麻烦的,当然也可以自己按上述格式写,但这么长的函数名很不爽。
也可以不用这样,但是要在dll或so加载的时候注册方法,而且要知道怎么算出函数的签名,java中加载dll、so的时候会严格比对签名。
如此写法:
int Init(JNIEnv *, jclass); // jni函数声明
注册函数:
// 注册函数列表
static JNINativeMethod s_methods[] = {
{"Init", "()I", (void *)Init},
};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
g_jvm = vm;
JNIEnv* env = NULL;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK)
{
return JNI_ERR;
}
// register jni method
jclass clz = env->FindClass("x/x/x"); // 包名,类名 com/test/jni/NativeAPI
if (clz == NULL) {
return JNI_ERR;
}
int len = sizeof(s_methods) / sizeof(s_methods[0]);
if (env->RegisterNatives(clz, s_methods, len) < 0) {
return JNI_ERR;
}
return JNI_VERSION_1_4;
}
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved)
{
JNIEnv* env = NULL;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK)
{
return;
}
}
2.java c/c++类型的转换
使用必定有很多java和类型和c类型的转换,比如java byte对应c的char等。
Java类型 本地类型 字节(bit)
boolean jboolean 8, unsigned
byte jbyte 8
char jchar 16, unsigned
short jshort 16
int jint 32
long jlong 64
float jfloat 32
double jdouble 64
void void n/a
3.函数签名的字符串的计算方法
方法的Signature是由方法的参数和返回值的类型共同构成的,
"(argument-types)return-type"
其中Java程序中参数类型和其对应的值如下:
Signature Java中的类型
Z boolean
B byte
C char
S short
I int
J long
F float
D double
L fully-qualified-class; fully-qualified-class
4.线程调用jvm资源
线程调用Jvm资源必须attach当前线程到jvm环境。
JNIEnv *env = NULL;
bool isAttach = false;
jint status = g_jvm->GetEnv((void **) &env, JNI_VERSION_1_4); // g_jvm全局变量在load是保存。
if(status != JNI_OK)
{
#if WIN32
status = g_jvm->AttachCurrentThread((void **)&env, NULL);
#else
status = g_jvm->AttachCurrentThread(&env, NULL);
#endif
if(status < 0) {
return;
}
isAttach = true;
}
// your code......
if (isAttach)
{
g_jvm->DetachCurrentThread();
}
5.有了上面几个步骤可以写基本的Jni了,但是要将java传入的非数值类型在Jni中使用、jni中返回Java可使用的字符串、jni中调用java的类还需要做很多准备。
关键一点要知道使用jni自带的函数调用后是全局还是局部应用,全局引用必须手动释放,局部引用在离开作用域后会自动解除引用,但是在作用域中也不能产生过多的局部应用,否则可能存在突破局部引用表上限。
6.回调java代码中的方法
java的方法写在class中,所以要调用java代码中的方法必须要向jni中传入类的对象。
jobject g_cbObj;
jmethodID g_midFun;
int GetMethod(JNIEnv *env, jclass, jobject obj)
{
// 获取java class
jclass cls = env->GetObjectClass(obj);
// 获取方法
g_midFun= env->GetMethodID(cls, "Callback_FUN", "(JJ)V"); // java方法 void Callback_FUN(long, long);
// 全局引用,回调的时候使用,彻底不用需要调用env->DeleteGlobalRef(g_cbObj);
g_cbObj= env->NewGlobalRef(UserContext);
}
回调Callback_FUN方法:
env->CallVoidMethod(g_cbObj, g_midFun, (jlong)1, (jlong)1);
就写这么多,细节还需要使用者在使用过程摸索。