JNI编程指南(一):基本类型、字符串、数组

前言

对于任何一个初学者,学习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来注册与一个类关联的本地方法。
  • JNIEXPORTJNICALL是定义在jni.h里面的两个宏,用来确保函数在本地库外可见,C编译时会进行正确转换。
  • JNIEnv是一个接口指针,指向若干个函数表,提供了JNI函数帮助C函数访问JVM里面的数据结构。
  • 本地方法是静态方法时,C函数的第二个变量是jclass,代表本地方法所在的类;如果是一个实例方法时,其变量的类型是jobject,代表本地方法所在的对象实例。

类型映射

本地方法声明中的参数类型在C语言中都有对应的类型,具体对应表格如下:

java类型本地类型描述
booleanjbooleanC/C++8位整型
bytejbyteC/C++带符号的8位整型
charjcharC/C++无符号的16位整型
shortjshortC/C++带符号的16位整型
intjintC/C++带符号的32位整型
longjlongC/C++带符号的64位整型e
floatjfloatC/C++32位浮点型
doublejdoubleC/C++64位浮点型
Objectjobject任何Java对象,或者没有对应java类型的对象
ClassjclassClass对象
Stringjstring字符串对象
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/ReleaseStringRegionGet/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更加深入的文章。

本文同步发布于简书CSDN

End!

    原文作者:码农叔叔
    原文地址: https://www.jianshu.com/p/47651ddfb3df
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞