JNI 程序中涉及了三种数据类型,分别是:
在 Java 程序中我们使用的是 Java 类型,C/C++ 程序中拿到的是 JNI 类型,我们需要将其转换为 C/C++ 类型,使用 C/C++ 类型再去调用 C/C++ 层函数完成计算或 IO 操作等任务后,将结果再转换为 JNI 类型返回后,在 java 代码中,我们就能收到对应的 Java 类型。
我们可以在 $JAVA_HOME/inlcude/jni.h 文件中查看到 jni 中基本类型的定义:
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble;
typedef jint jsize;
$JAVA_HOME/include/linux/jni_md.h 中定义了 jbyte, jint and jlong 和 CPU 平台相关的类型:
typedef int jint;
#ifdef _LP64
typedef long jlong;
#else
typedef long long jlong;
#endif
typedef signed char jbyte;
以上这些类型我们称之为基本数据类型,其关系梳理如下:
Java 类型 | JNI 类型 | C/C++ 类型 |
---|---|---|
boolean | jboolean | unsigned char |
byte | jbyte | signed char |
char | jchar | unsigned short |
short | jshort | signed short |
int | jint | int |
long | jlong | long |
float | jfloat | float |
double | jdouble | double |
这些类型不需要进行转换,可以直接在 JNI 中使用:
jbyte result=0xff;
jint size;
jbyte* timeBytes;
引用类型也定义在 jni.h 中:
#ifdef __cplusplus
class _jobject {};
class _jclass : public _jobject {};
class _jthrowable : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jobjectArray : public _jarray {};
typedef _jobject *jobject;
typedef _jclass *jclass;
typedef _jthrowable *jthrowable;
typedef _jstring *jstring;
typedef _jarray *jarray;
typedef _jbooleanArray *jbooleanArray;
typedef _jbyteArray *jbyteArray;
typedef _jcharArray *jcharArray;
typedef _jshortArray *jshortArray;
typedef _jintArray *jintArray;
typedef _jlongArray *jlongArray;
typedef _jfloatArray *jfloatArray;
typedef _jdoubleArray *jdoubleArray;
typedef _jobjectArray *jobjectArray;
#else
struct _jobject;
typedef struct _jobject *jobject;
typedef jobject jclass;
typedef jobject jthrowable;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;
#endif
总结如下:
java 类型 | JNI 引用类型 | 类型描述 |
---|---|---|
java.lang.Object | jobject | 表示任何Java的对象 |
java.lang.String | jstring | Java的String字符串类型的对象 |
java.lang.Class | jclass | Java的Class类型对象 |
java.lang.Throwable | jthrowable | Java的Throwable类型 |
byte[] | jbyteArray | Java byte型数组 |
Object[] | jobjectArray | Java任何对象的数组 |
boolean[] | jbooleanArray | Java boolean型数组 |
char[] | jcharArray | Java char型数组 |
short[] | jshortArray | Java short型数组 |
int[] | jintArray | Java int型数组 |
long[] | jlongArray | Java long型数组 |
float[] | jfloatArray | Java float型数组 |
double[] | jdoubleArray | Java double型数组 |
native 程序主要做了这么几件事:
其中很多代码都是在做类型转换的操作,下面我们来看看类型转换的示例。
基本类型无需做转换,直接使用:
java 层:
private native double average(int n1, int n2);
c/c++ 层:
JNIEXPORT jdouble JNICALL Java_HelloJNI_average(JNIEnv *env, jobject jobj, jint n1, jint n2) {
//基本类型不用做转换,直接使用
cout << "n1 = " << n1 << ", n2 = " << n2 << endl;
return jdouble(n1 + n2)/2.0;
}
为了在 C/C++ 中使用 Java 字符串,需要先将 Java 字符串转换成 C 字符串。用 GetStringChars 函数可以将 Unicode 格式的 Java 字符串转换成 C 字符串,用 GetStringUTFChars 函数可以将 UTF-8 格式的 Java 字符串转换成 C 字符串。这些函数的第三个参数均为 isCopy,它让调用者确定返回的 C 字符串地址指向副本还是指向堆中的固定对象。
java 层:
private native String sayHello(String msg);
c/c++ 层:
jJNIEXPORT jstring JNICALL Java_HelloJNI_sayHello__Ljava_lang_String_2(JNIEnv *env, jobject jobj, jstring str) {
//jstring -> char*
jboolean isCopy;
//GetStringChars 用于 unicode 编码
//GetStringUTFChars 用于 utf-8 编码
const char* cStr = env->GetStringUTFChars(str, &isCopy);
if (nullptr == cStr) {
return nullptr;
}
if (JNI_TRUE == isCopy) {
cout << "C 字符串是 java 字符串的一份拷贝" << endl;
} else {
cout << "C 字符串指向 java 层的字符串" << endl;
}
cout << "C/C++ 层接收到的字符串是 " << cStr << endl;
//通过JNI GetStringChars 函数和 GetStringUTFChars 函数获得的C字符串在原生代码中
//使用完之后需要正确地释放,否则将会引起内存泄露。
env->ReleaseStringUTFChars(str, cStr);
string outString = "Hello, JNI";
// char* 转换为 jstring
return env->NewStringUTF(outString.c_str());
}
数组的操作与字符串类似:
java 层:
private native double[] sumAndAverage(int[] numbers);
c++ 层:
JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAverage(JNIEnv *env, jobject obj, jintArray inJNIArray) {
//类型转换 jintArray -> jint*
jboolean isCopy;
jint* inArray = env->GetIntArrayElements(inJNIArray, &isCopy);
if (JNI_TRUE == isCopy) {
cout << "C 层的数组是 java 层数组的一份拷贝" << endl;
} else {
cout << "C 层的数组指向 java 层的数组" << endl;
}
if(nullptr == inArray) return nullptr;
//获取到数组长度
jsize length = env->GetArrayLength(inJNIArray);
jint sum = 0;
for(int i = 0; i < length; ++i) {
sum += inArray[i];
}
jdouble average = (jdouble)sum / length;
//释放数组
env->ReleaseIntArrayElements(inJNIArray, inArray, 0); // release resource
//构造返回数据,outArray 是指针类型,需要 free 或者 delete 吗?要的
jdouble outArray[] = {sum, average};
jdoubleArray outJNIArray = env->NewDoubleArray(2);
if(NULL == outJNIArray) return NULL;
//向 jdoubleArray 写入数据
env->SetDoubleArrayRegion(outJNIArray, 0, 2, outArray);
return outJNIArray;
}
其他类型的装换都大体类似,大家可以举一反三。
我们先回顾一下 Native 层和 Java 层里对象的创建和销毁的过程
JNI 层作为 Java 层和 Native 层之间相交互的中间层,它兼具 Native 层和 Java 层的某些特性,尤其在对引用对象的创建和回收上。
引用类型针对的是除开基本类型的 JNI 类型,比如 jstring, jclass ,jobject 等。JNI 类型是 java 层与 c 层的中间类型,java 层与 c 层都需要管理他。我们可以将 JNI 引用类型理解为 Java 意义的对象。
JNI 类型根据使用的方式可分为:
什么是局部引用?
通过 JNI 接口从 Java 传递下来或者通过 NewLocalRef 和各种 JNI 接口(FindClass、NewObject、GetObjectClass和NewCharArray等)创建的引用称为局部引用。
局部引用的特点?
一个常见的错误是使用静态变量保存局部引用,试图缓存变量提高性能:
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj)
{
static jclass cls_string = NULL;
if (cls_string == NULL) {
cls_string = (*env)->FindClass(env, "java/lang/String");
if (cls_string == NULL) {
return NULL;
}
}
return (*env)->NewStringUTF(env,"Hello from JNI !");
}
cls_string 是一个局部引用,当 native 函数执行完成后,gc 可能会回收掉 cls_string 指向的内存。下次调用该函数时,cls_string 存储的就是一个被释放后的内存地址,成了一个野指针。严重的,造成非法地址的访问,程序崩溃。
释放一个局部引用有两种方式:
既然 JVM 会在函数返回后会自动释放所有局部引用,为什么还需要手动释放呢? 以下几种情况下,为了避免内存溢出,我们应该手动释放局部引用:
言而总之,当一个局部引用不在使用后,立即将其释放,以避免不必要的内存浪费。
JNI 的规范指出,JVM 要确保每个 Native 方法至少可以创建 16 个局部引用,经验表明,16 个局部引用已经足够平常的使用了。
但是,如果要与 JVM 的中对象进行复杂的交互计算,就需要创建更多的局部引用了,这时就需要使用 EnsureLocalCapacity
来确保可以创建指定数量的局部引用,如果创建成功返回 0 ,返回返回小于 0 ,如下代码示例:
// Use EnsureLocalCapacity
int len = 20;
if (env->EnsureLocalCapacity(len) < 0) {
// 创建失败,out of memory
}
for (int i = 0; i < len; ++i) {
jstring jstr = env->GetObjectArrayElement(arr,i);
// 处理 字符串
// 创建了足够多的局部引用,这里就不用删除了,显然占用更多的内存
}
确保可以创建了足够的局部引用数量,所以在循环处理局部引用时可以不进行删除了,但是显然会消耗更多的内存空间了。
PushLocalFrame 与 PopLocalFrame 是两个配套使用的函数对。它们可以为局部引用创建一个指定数量内嵌的空间,在这个函数对之间的局部引用都会在这个空间内,直到释放后,所有的局部引用都会被释放掉,不用再担心每一个局部引用的释放问题了。
常见的使用场景就是在循环中:
// Use PushLocalFrame & PopLocalFrame
for (int i = 0; i < len; ++i) {
if (env->PushLocalFrame(len)) { // 创建指定数据的局部引用空间
//out ot memory
}
jstring jstr = env->GetObjectArrayElement(arr, i);
// 处理字符串
// 期间创建的局部引用,都会在 PushLocalFrame 创建的局部引用空间中
// 调用 PopLocalFrame 直接释放这个空间内的所有局部引用
env->PopLocalFrame(NULL);
}
使用 PushLocalFrame & PopLocalFrame 函数对,就可以在期间放心地处理局部引用,最后统一释放掉。
全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效。同局部引用一样,也会阻止它所引用的对象被 GC 回收。与局部引用不一样的是,函数执行完后,GC 也不会回收全局引用指向的对象。与局部引用创建方式不同的是,只能通过 NewGlobalRef 函数创建。
static jclass cls_string = NULL;
if (cls_string == NULL) {
jclass local_cls_string = (*env)->FindClass(env, "java/lang/String");
if (cls_string == NULL) {
return NULL;
}
// 将java.lang.String类的Class引用缓存到全局引用当中
cls_string = (*env)->NewGlobalRef(env, local_cls_string);
// 删除局部引用
(*env)->DeleteLocalRef(env, local_cls_string);
// 再次验证全局引用是否创建成功
if (cls_string == NULL) {
return NULL;
}
}
当我们的本地代码不再需要一个全局引用时,应该马上调用 DeleteGlobalRef
来释放它。如果不手动调用这个函数,即使这个对象已经没用了,JVM 也不会回收这个全局引用所指向的对象。
弱全局引用使用 NewGlobalWeakRef
创建,使用 DeleteGlobalWeakRef
释放。下面简称弱引用。与全局引用类似,弱引用可以跨方法、线程使用。但与全局引用很重要不同的一点是,弱引用不会阻止 GC 回收它引用的对象。
static jclass myCls2 = NULL;
if (myCls2 == NULL)
{
jclass myCls2Local = (*env)->FindClass(env, "mypkg/MyCls2");
if (myCls2Local == NULL)
{
return; /* 没有找到mypkg/MyCls2这个类 */
}
myCls2 = NewWeakGlobalRef(env, myCls2Local);
if (myCls2 == NULL)
{
return; /* 内存溢出 */
}
}
... /* 使用myCls2的引用 */
IsSameObject
用来判断两个引用是否指向相同的对象。还可以用 isSameObject
来比较弱全局引用所引用的对象是否被 GC 了,返回 JNI_TRUE 则表示回收了,JNI_FALSE 则表示未被回收。
env->IsSameObject(obj1, obj2) // 比较两个引用是否指向相同的对象
env->IsSameObject(obj, NULL) // 比较局部引用或者全局引用是否为 NULL
env->IsSameObject(wobj, NULL) // 比较弱全局引用所引用对象是否被 GC 回收
一些疑问
如果 C 层返回给 java 层一个全局引用,这个全局引用何时可以被 GC 回收?
我认为不会被 GC 回收,造成内存泄漏。
所以 JNI 函数如果要返回一个对象,我们应该使用局部引用作为返回值。
阅读量:952
点赞量:0
收藏量:0