定义和注册Native方法
本章介绍定义和注册Native方法的相关知识。在开发Native代码之前,先创建一个Java Class,在这个Class中声明与Native代码中对应的方法:
package com.examp.jni;
class HelloJNI {
static {
System.loadLibrary("hello-jni");
}
public native boolean hello();
}
定义Native方法
定义Native的方法有两种,区别于是映射Native方法与java方法的方式。如果使用函数命名规则来匹配和映射,则只需定义Native方法,JVM在Load Library时通过命名规则去搜索匹配的Native方法;如果采用运行时动态注册(使用RegisterNatives)的方式则需要先定义Native方法然后在JNI_OnLoad方法内注册NativeMethods。
因为JNI中Native方法与Java方法的映射可通过函数名规则来匹配,如果使用这种方法,可使用javah工具生成C/C++头文件。
javac HelloJni.java
javah HelloJni
当然,如果你熟悉JNI函数名映射规则,你可以自己手动创建。使用函数名称来映射Native方法与Java方法的规则如下:
JNIEXPORT ret-type JNICALL Java_{package-and-classname}_{function-name}(jni args)
JNIEXPORT jboolean JNICALL Java_com_examp_jni_HelloJNI_hello(JNIEnv *env, jobject obj);
JNI方法的形参列表至少包含JNIEnv *, jobjcet两个参数:
* JNIEnv *env: JNI Environment的引用,是用env便可以调用所有JNI提供的函数。
* jobjcet obj: 调用该JNI函数的对象的引用。
方法可以有更多的参数,如果有,紧跟以上两个参数后面即可。
注册Native方法
在JNI Overview章节中我们提到,除了使用函数名称映射Native方法与Java方法,还可以在运行时动态注册。使用动态注册的方式简化了函数名称,并且可以动态的更新映射关系。动态创建映射关系步骤如下:
1.在Native代码中定义方法
jboolean hello() {
// do something
return JNI_TRUE;
}
2.创建JNINativeMethod数组
static JNINativeMethod jniMethods[] = {
{"hello", "()Z", (void *)hello},
};
应该按照以上格式将所有需要动态注册的方法填充至JNINativeMethod数组,JNINativeMethod结构有三个成员:
* const char *name: Java中声明的native方法。
* const char *signature:方法的签名。
* void *fnPtr: 函数指针
3.在JNI_OnLoad中调用RegisterNatives方法注册Natives方法到JVM,建立映射关系。
int JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv *env;
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4) != JNI_OK) {
return JNI_ERR;
}
jclass cls = (*env)->FindClass(env, "LHelloJNI");
if (cls == NULL)
return JNI_ERR;
int len = sizeof(jniMethods) / sizeof(jnimethods[0]);
(*env)->RegisterNatives(env, cls, jniMethods, len);
return JNI_VERSION_1_4;
}
JNI方法使用指导
Get ID
很多应用场景下,我们需要读写Java中的成员变量或调用Java中成员函数,实现这一功能的第一步是先或者目标(Java变量或函数)的ID。 JNI Environment提供一组用于获取这些ID的接口。
GetFieldID, GetStaticFieldID
jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
GetFieldID返回非静态Java成员变量的ID,通过name和签名来匹配成员变量。GetStaticFieldID返回静态的Java变量的ID。失败返回NULL。
调用这两个方法均会导致未初始化的类被初始化。
GetMethodID, GetStaticMethodID
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
GetMethodID返回类的非静态成员方法的ID,GetStaticMethodID返回类的静态方法。失败返回NULL。
调用这两个方法均会导致未初始化的类被初始化。
Call Static/Non-Static method
Native调用Java类的Static/Non-Static方法需通过以下6个JNI方法实现。
NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args);
NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);
NativeType Call<type>MethodA(JNIEnv *env, jobject obj, jmethodID methodID, jvalue *args);
NativeType Call<type>MethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
Type类型有:Object、Boolean、Byte、Char、Short、Int、Long、Float、Double、Void。注意type指的是返回值的类型。
Static: Method ID对应的方法必须是clazz类或者其派生类的方法,不能是其父类的方法。
Non-Static: Method ID对应的方法需是obj对象所在类或者其派生类的方法,不能是其父类的方法。
以上三个JNI接口方法均用于调用非静态的Java方法,区别在于传递参数的形式不同。使用CallMethod需将所有参数完整列出;使用CallMethodA需将参数保存在jvalue*列表里;使用CallMethodV需将参数保存在va_list结构中。
可以看到Call Static Method和Call Nonstatic Method接口函数的第二个形参类型不同,因为static方法不需要有实际对象也可以调用,所以前者是jcalss后者是jobjcet。
Get/Set Static/Non-Static Field
Native对应Java成员变量的操作可通过以下4组JNI方法实现:
NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID);
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);
void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value);
void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);
Type类型有:Object、Boolean、Byte、Char、Short、Int、Long、Float、Double、Void。
Get/Release Array Elements
在Native层需要与Java层通过数据交互数据的应用场景下,JNI提供如下的方法来获得和释放对Java数组的引用。
NativeType *Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);
void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);
PrimitiveType:Boolean,Byte, Char, Short, Int, Long, Float, Double。
Get Array Elements
Get方法用于获取对ArrayType的引用,isCopy指示JVM是否创建一份数组的拷贝,如果isCopy不为NULL,当成功创建拷贝的时候isCopy被置为JNI_TRUE,否则被置为JNI_FALSE。如果创建了数组的拷贝,则Native代码对数组内容的修改将会在调用Release方法后才会生效。如果isCopy为NULL,则JVM返回实际数组的引用。
Release Array Elements
Release方法通知JVM Native代码不再需要操作elems,elems是通过Get方法获得ArrayType引用的指针,JVM将根据参数决定是否要将elems中的修改写回到Java数组中以及如何释放缓冲区。
Mode参数指示如何释放缓冲区,它有如下三种值,在大多数应用场景下传递0使得对数组的修改生效且释放elems缓冲区。
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
Note:Get方法与Release方法需成对出现,因为每调用一次Get方法JVM将创建一份Local Reference,JVM能否创建的Local Reference的数量是有限的,如果大量调用Get方法而不调用Release方法将会导致溢出,JVM会抛出异常致使应用程序停止。
Get/Set Array Region
如果只需要重Java数组中复制数据到Native数组中,则使用GetArrayRegion,这将省去Release操作。
void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, const NativeType *buf);
PrimitiveType:Boolean,Byte, Char, Short, Int, Long, Float, Double。
Get方法:
参数array是待Copy的java数组,通常由Java代码通过形参传递到Native方法;start即Copy的起始索引,NativeType是Copy的目的地址。JNI将Copy array数组中的数据到buf所指向的内存里。
Set方法:
参数array Set操作的目标数组,通常由Java代码通过形参传递到Native方法;start即Copy的起始索引,NativeType是Copy的目的地址。JNI将把buf所指向的内存中的数据Copy到array数组中start位置。
Note: Get Array Region方法不需要任何Release操作。
Global Weak and Local Reference
当Native代码需要获得Java对象的引用时,则需要使用以下JNI方法。
jobject NewGlobalRef(JNIEnv *env, jobject obj);
void DeleteGlobalRef(JNIEnv *env, jobject globalRef);
jobject NewLocalRef(JNIEnv *env, jobject ref);
void DeleteLocalRef(JNIEnv *env, jobject localRef);
jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);
以上API用于创建Global/Local/Weak引用。New操作返回对象的引用,或者返回NULL代表对象已经被释放掉了。
三种类型的区别:
- Global:全局有效,必须显示的Dlete,否则对象将一直不会被JVM回收。
- Local:Local引用只在调用Native方法期间有效,虽然Local Reference会在Native方法调用结束后被释放掉,但任然推荐显示的释放Local Reference。
- Weak:弱全局引用是特殊的全局引用,弱全局引用不增加的引用计数,允许被引用的对象随时被JVM回收掉。因此弱全局引用在使用的时候应先调用
IsSameObject
与NULL比较,若为NULL则代表对象已经被JVM回收掉了。
虽然IsSameObject可以用于判断WakeGlobalRef引用的对象是否已经被回收,但它无法阻止JVM回收对象,因此开发人员不应该依赖此方法来决定WakeGlobalRef是否可用,对象任然可能在任意时刻被JVM回收掉。
官方推荐的做法是:在使用WakeGlobalRef之前先使用GlobalRef或者LocalRef获得对象的引用,如果成功获得(返回值不为NULL)则将阻止JVM回收该对象。
String Operations
Native代码中可以针对String做一些操作,包括:
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize len);
jsize GetStringLength(JNIEnv *env, jstring string);
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);
jstring NewStringUTF(JNIEnv *env, const char *bytes);
jsize GetStringUTFLength(JNIEnv *env, jstring string);
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize len, jchar *buf);
jsize GetArrayLength(JNIEnv *env, jarray array);
Note: Java字符和字符串(String,Char)使用Unicode编码,16bits,而C/C++中字符串使用UTF-8编码,8bits,因此在使用时需注意转换。
参考资料: