美文网首页Android开发经验谈Android技术知识Android-NDK/JNI
JNI编程指南(一):基本类型、字符串、数组

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

作者: 码农叔叔 | 来源:发表于2018-11-01 23:25 被阅读14次

    前言

    对于任何一个初学者,学习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类型 本地类型 描述
    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/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!

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/xbintqtx.html