1. 数据类型
JNI 程序中涉及了三种数据类型,分别是:
- Java 类型
- JNI 类型
- C/C++ 类型
在 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型数组 |
2. 数据类型转换
native 程序主要做了这么几件事:
- 接收 JNI 类型的参数
- 参数类型转换,JNI 类型转换为 Native 类型
- 执行 Native 代码
- 创建一个 JNI 类型的返回对象,将结果拷贝到这个对象并返回结果
其中很多代码都是在做类型转换的操作,下面我们来看看类型转换的示例。
2.1 基本类型
基本类型无需做转换,直接使用:
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;
}
2.2 字符串
为了在 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());
}
2.3 数组
数组的操作与字符串类似:
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;
}
其他类型的装换都大体类似,大家可以举一反三。
3. 引用类型
我们先回顾一下 Native 层和 Java 层里对象的创建和销毁的过程
- 以 C++ 为例,Native 层中要创建一个对象的话需使用 new 操作符先分配内存,然后构造对象。如果不再使用这个对象,则需要通过 作符先析构这个对象,然后回收该对象所占的内存。
- Java 层中也通过 new 操作来构造一个对象。如果后续不再使用它,则可以显式地设置持有这个对象的变量的值为 null(也可以不做这一步,而交由垃圾回收来扫描和标记该对象是否有被引用)。该对象所占的内存则在垃圾回收过程中被收回。
JNI 层作为 Java 层和 Native 层之间相交互的中间层,它兼具 Native 层和 Java 层的某些特性,尤其在对引用对象的创建和回收上。
- 和 C++ 里的 new 操作符可以创建一个对象类似,JNI 层可以利用 JNI NewObject 等函数创建一个 Java 意义的对象(引用型对象)。这个被 New 出来的对象是局部(Local) 型的引用对象。
- JNI 层可通过 DeleteLocalRef 释放 Local 型的引用对象(等同于Java 层中设置持有这个对象的变量的值为 null)。如果不调用 DeleteLocalRef 的话,根据 JNI 规范,Local 型对象在 JNI 函数返回后,也会由虚拟机根据垃圾回收的逻辑进行标记和回收。
- 除了 Local 型对象外,JNI 层借助JNI Global 相关函数可以将一个 Local 型引用对象转换成一个全局(Global) 型对象。而 Global 型对象的回收只能先由程序显式地调用 Global 相关函数进行删除,然后,虚拟机才能借助垃圾回收机制回收它们
引用类型针对的是除开基本类型的 JNI 类型,比如 jstring, jclass ,jobject 等。JNI 类型是 java 层与 c 层的中间类型,java 层与 c 层都需要管理他。我们可以将 JNI 引用类型理解为 Java 意义的对象。
JNI 类型根据使用的方式可分为:
- 局部引用
- 全部引用
- 弱全部引用
3.1 局部引用
3.1.1 局部引用与概念
什么是局部引用?
通过 JNI 接口从 Java 传递下来或者通过 NewLocalRef 和各种 JNI 接口(FindClass、NewObject、GetObjectClass和NewCharArray等)创建的引用称为局部引用。
局部引用的特点?
- 在函数为执行完毕前,局部引用会阻止 GC 回收所引用的对象
- 局部引用不能在本地函数中跨函数使用,不能跨线前使用,当然也不能直接缓存起来使用
- 函数返回后(未返回局部引用的情况下),局部引用所引用的对象会被 JVM 自动释放,也可在函数结束前通过 DeleteLocalRef 函数手动释放
- 如果 c 函数返回了一个局部引用数据,在 java 层,该类型会转换为对应的 java 类型。当 java 层不存在该对象的引用时,gc 就会回收该对象
一个常见的错误是使用静态变量保存局部引用,试图缓存变量提高性能:
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 存储的就是一个被释放后的内存地址,成了一个野指针。严重的,造成非法地址的访问,程序崩溃。
3.1.2 释放局部变量
释放一个局部引用有两种方式:
- 本地方法执行完毕后 JVM 自动释放,
- 自己调用 DeleteLocalRef 手动释放
既然 JVM 会在函数返回后会自动释放所有局部引用,为什么还需要手动释放呢? 以下几种情况下,为了避免内存溢出,我们应该手动释放局部引用:
- JNI 会将创建的局部引用都存储在一个局部引用表中,如果这个表超过了最大容量限制,就会造成局部引用表溢出,使程序崩溃。经测试,Android上的 JNI 局部引用表最大数量是 512 个。当我们在实现一个本地方法时,可能需要创建大量的局部引用,如果没有及时释放,就有可能导致 JNI 局部引用表的溢出,所以,在不需要局部引用时就立即调用 DeleteLocalRef 手动删除。
for (i = 0; i < len; i++) {
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* 使用jstr */
(*env)->DeleteLocalRef(env, jstr); // 使用完成之后马上释放
}
- 在编写 JNI 工具函数时,工具函数在程序当中是公用的,被谁调用你是不知道的。其内部的局部引用在使用完成后应该立即释放,避免过多的内存占用。
- 如果你的本地函数不会返回。比如一个接收消息的函数,里面有一个死循环,用于等待别人发送消息过来
while(true) { if (有新的消息) { 处理之。。。。} else { 等待新的消息。。。}}
。如果在消息循环当中创建的引用你不显示删除,很快将会造成JVM局部引用表溢出。 - 局部引用使用完了就删除,而不是要等到函数结尾才释放,局部引用会阻止所引用的对象被 GC 回收。比如你写的一个本地函数中刚开始需要访问一个大对象,因此一开始就创建了一个对这个对象的引用,但在函数返回前会有一个大量的非常复杂的计算过程,而在这个计算过程当中是不需要前面创建的那个大对象的引用的。但是,在计算的过程当中,如果这个大对象的引用还没有被释放的话,会阻止 GC 回收这个对象,内存一直占用者,造成资源的浪费。所以这种情况下,在进行复杂计算之前就应该把引用给释放了,以免不必要的资源浪费。
言而总之,当一个局部引用不在使用后,立即将其释放,以避免不必要的内存浪费。
3.1.3 局部引用的管理
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 函数对,就可以在期间放心地处理局部引用,最后统一释放掉。
3.2 全局引用
全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效。同局部引用一样,也会阻止它所引用的对象被 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 也不会回收这个全局引用所指向的对象。
3.3 弱全局引用
弱全局引用使用 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的引用 */
3.4 引用比较
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 函数如果要返回一个对象,我们应该使用局部引用作为返回值。
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/20392,转载请注明出处。
评论0