前言
对于任何一个初学者,学习JNI都是从Java和C/C++之间如何传递数据,以及数据类型之间是如何相互映射开始。
Native方法和C函数原型
看点代码
package com.net168.xxx
class Simple {
private native String testA(String str);
private native static void testA(int num);
}
//C端源码
JNIEXPORT jstring JNICALL
Java_Com_net168_xxx_Simple_testA(JNIEnv *env, jobject thiz, jstring str);
JNIEXPORT void JNICALL
Java_Com_net168_xxx_Simple_testB(JNIEnv *env, jclass clz, jint num);
知识点
- C函数方法格式:
JNIEXPORT 返回类型 JNICALL Java_包名_方法名(JNIEnv *env, jobect/jclass thiz, 入参列表)
- 本地方法存在重载情况时,会有双下划线"__",后面跟着参数描述符,也就是长函数名;VM连接优先链接短函数名,然后链接长函数名,如果存在两个重载的本地方法,则只会链接长函数名。
- 链接函数还可以通过JNI的
RegisterNatives
来注册与一个类关联的本地方法。 -
JNIEXPORT
和JNICALL
是定义在jni.h
里面的两个宏,用来确保函数在本地库外可见,C编译时会进行正确转换。 -
JNIEnv
是一个接口指针,指向若干个函数表,提供了JNI函数帮助C函数访问JVM里面的数据结构。 - 本地方法是静态方法时,C函数的第二个变量是jclass,代表本地方法所在的类;如果是一个实例方法时,其变量的类型是jobject,代表本地方法所在的对象实例。
类型映射
本地方法声明中的参数类型在C语言中都有对应的类型,具体对应表格如下:
java类型 | 本地类型 | 描述 |
---|---|---|
boolean | jboolean | C/C++8位整型 |
byte | jbyte | C/C++带符号的8位整型 |
char | jchar | C/C++无符号的16位整型 |
short | jshort | C/C++带符号的16位整型 |
int | jint | C/C++带符号的32位整型 |
long | jlong | C/C++带符号的64位整型e |
float | jfloat | C/C++32位浮点型 |
double | jdouble | C/C++64位浮点型 |
Object | jobject | 任何Java对象,或者没有对应java类型的对象 |
Class | jclass | Class对象 |
String | jstring | 字符串对象 |
Object[] | jobjectArray | 任何对象的数组 |
boolean[] | jbooleanArray | 布尔型数组 |
byte[] | jbyteArray | 比特型数组 |
char[] | jcharArray | 字符型数组 |
short[] | jshortArray | 短整型数组 |
int[] | jintArray | 整型数组 |
long[] | jlongArray | 长整型数组 |
float[] | jfloatArray | 浮点型数组 |
double[] | jdoubleArray | 双浮点型数组 |
知识点
- Java里面有两种类型:基本类型和引用类型,JNI对这两个类型的处理方式是不同的。
- JNI把Java中的对象当做一个C指针传递到本地方法中,这个指针指向JVM的内部数据结构,也就是其在内存中的储存方式是不可见的,必须通过JNI函数来操作JVM中的对象。
字符串处理
jstring转c语言字符串
JNIEXPORT jstring JNICALL
Java_Com_net168_xxx_Simple_testA(JNIEnv *env, jobject thiz, jstring jstr)
{
jboolean isCopy;
//获取utf-8格式的c字符串
const char *str1 = env->GetStringUTFChars(jstr, &isCopy);
//do something
env->ReleaseStringUTFChars(jstr, str1);
//获取Unicode格式的c字符串
const jchar *str2 = env->GetStringChars(jstr, &isCopy);
//do something
env->ReleaseStringChars(jstr, str2);
}
知识点
-
GetStringUTFChars()
可以将jstring转换成UTF-8编码格式的c字符串,GetStringChars()
可以将jstring转换成Unicode编码格式的c字符串。 - 获取c字符串需要判断
if(str == NULL)
,原因可能是JVM需要为这个字符串分配内存,会由于内存不足导致失败,抛出OutOfMemoryError
异常。 - 对于第二个参数
isCopy
,如果c字符串是指向JVM中jstring的同一份数据时为JNI_FALSE
;如果c字符串是jstring的一份内存拷贝则为JNI_TRUE
。若为JNI_FALSE
我们不可能修改该c字符串,会破坏Java语言String不可变的原则。一般我们不需要关心是否复制的,那么可以传入NULL
。 - 一旦Java对象指针被传递给c代码,那么GC就不会回收这个对象;所以我们需要调用
ReleaseStringUTFChars()
/ReleaseStringChars()
这两个方法来释放资源:如果是获取了jstring的直接引用,则解除JVM的持有让GC可以回收;如果是内存拷贝则回收释放相应内存。 - utf-8字符串以
\0
结尾,而Unicode不是;所以当ReleaseStringUTFChars()
获取一个编码格式为Unicode的jstring时,返回的c字符串并不一定以\0
结尾。建议直接以GetStringLength()
和GetStringUTFLength()
来获取字符串长度;对于strlen()
需要谨慎确保jstring指向的是一个utf-8的字符串。
构造新字符串
const char *str = "hello";
//将str转为utf-8编码的jstring字符串
jstring jstr = env->NewStringUTF(str);
const jchar *str1 = env->GetStringChars(jstr, NULL);
//将str1转为unicode编码的jstring字符串
jstring jstr1 = env->NewString(str1, env->GetStringLength(jstr));
知识点
- 获取c字符串需要判断
if(jstr == NULL)
,如果JVM内存不足则会抛出OutOfMemoryError异常,并返回NULL。 -
NewStringUTF()
不需要传入字符串长度,因为utf-8默认以/0
结尾;而NewString()
则需要在第二个参数传入该字符串的长度。
其他字符串函数
//临界区字符串函数
const jchar *str = env->GetStringCritical(jstr, NULL);
//do something
env->ReleaseStringCritical(jstr, str);
//预先分配缓存字符串函数
jchar *str1 = static_cast<jchar *>(malloc(5 * sizeof(jchar)));
env->GetStringRegion(jstr, 0, 5, str1);
//do something
//自己释放str1 malloc的内存
free(str1);
知识点
-
Get/ReleaseStringCritical
可以提高JVM返回直接指针的可能性,其会禁止GC的运行,但是其必须运行在"临界区"中,也就是在这两函数中间不能调用任何线程阻塞、或者本地JNI函数,否则容易引起死锁。 -
Get/ReleaseStringRegion
和Get/ReleaseStringUTFRegion
对于小字符串来说是最佳选择,因为缓冲区可以提前分配;并且可以按需复制小段内容,因为它提供了一个开始索引和子字符串长度。
数组
基本类型数据数组
JNIEXPORT void JNICALL
Java_Com_net168_xxx_Simple_testC(JNIEnv *env, jobject thiz, jintArray jarray)
{
//获取整个数组内容
jint *array1 = env->GetIntArrayElements(jarray, NULL);
//do something
env->ReleaseIntArrayElements(jarray, array1, 0);
//获取数组长度
jsize len = env->GetArrayLength(jarray);
//预分配获取数组内容
jint buf[10];
env->GetIntArrayRegion(jarray, 0, 10, buf);
//栈区域不用手动释放内存
//在开始索引3的位置,开始更新5个数据
env->SetIntArrayRegion(jarray, 3, 5, buf);
//临界区获取数组内容
jint *array2 = static_cast<jint *>(env->GetPrimitiveArrayCritical(jarray, NULL));
//do something
env->ReleasePrimitiveArrayCritical(jarray, array2, 0);
}
知识点
-
Get/Release<Type>ArrayElements
函数可以获取到一个指向基本类型<Type>的指针,其可能指向jarray的同一份数据,而已进行内存的拷贝后返回;如果字符串处理一样,我们最后需要Release来释放资源。 -
GetArrayLength
返回数组中的个数,这个在数组首次分配时确定下来。 -
Set/Get<Type>ArrayRegion
可以在预先分配的c缓存区和jvm交换数据,函数还可以指定一个索引和长度对子数组进行操作。 -
Get/ReleasePrimitiveArrayCritical
能提高返回直接指针的可能性,但是需要注意不能再临界区让线程阻塞或者使用其他jni函数,可能会导致死锁的发生。
对象数组
JNIEXPORT void JNICALL
Java_Com_net168_xxx_Simple_testD(JNIEnv *env, jobject thiz, jobjectArray jarray)
{
//获取jobjectArray的第一个jobject
jobject obj1 = env->GetObjectArrayElement(jarray, 0);
//将obj1设置到数组的第二个索引的位置
env->SetObjectArrayElement(jarray, 1, obj1);
}
知识点
- 对象数组不能一次性获取整个数组,需要用
GetObjectArrayElement
获取指定索引位置的jobect对象,还有用SetObjectArrayElement
修改数组指定位置的元素。
结语
后续会陆续发布多篇JNI更加深入的文章。
End!
网友评论