JNI&NDK开发最佳实践(六):JNI实现本地方法时的数据类型转换

前言

通过《JNI数据类型及与Java数据类型的映射关系》一文,我们知道了在C中实现java声明的本地方法时,方法入参有其一一对应关系。但在实现方法的方法体中,这些数据类型是否可以拿来直接使用呢?
举个例子,在java文件中声明本地方法如下

public static native void test(short s, int i, long l, float f, double d, char c,
boolean z, byte b, String str, Object obj, MyClass p, int[] arr);

其在c中的对应实现如下

JNIEXPORT void JNICALL Java_com_study_jnilearn_HelloWorld_test
(JNIEnv *env, jclass cls, jshort s, jint i, jlong l, jfloat f,
jdouble d, jchar c, jboolean z, jbyte b, jstring j_str, jobject jobj1, jobject job2, jintArray j_int_arr)
{
...
}

-基本数据类型如jint,jdouble是否可以直接操作?
-可以。
-引用数据类型如jstring,jxxArray是否可以直接操作?
-不可以。JNI接口提供了许多函数来进行这样的转换。

1. C方法中操作jstring

test函数接收一个jstring类型的参数str,但jstring类型是指向JVM内部的一个字符串,和C风格的字符串类型char* 不同,所以在JNI中不能通把jstring当作普通C字符串一样来使用,必须使用合适的JNI函数来访问JVM内部的字符串数据结构。
因为Java默认使用Unicode编码,而C/C++默认使用UTF编码,所以在本地代码中操作字符串的时候,必须使用合适的JNI函数把jstring转换成C风格的字符串。JNI支持字符串在Unicode和UTF-8两种编码之间转换。
GetStringUTFChars可以把一个jstring指针(指向JVM内部的Unicode字符序列)转换成一个UTF-8格式的C字符串。在上例中test函数中我们通过GetStringUTFChars正确取得了JVM内部的字符串内容。

1.1 操作步骤

  1. GetStringUTFChars(env, j_str, &isCopy) 访问字符串。
    isCopy:取值JNI_TRUE和JNI_FALSE,如果值为JNI_TRUE,表示返回JVM内部源字符串的一份拷贝,并为新产生的字符串分配内存空间。如果值为JNI_FALSE,表示返回JVM内部源字符串的指针,意味着可以通过指针修改源字符串的内容,不推荐这么做,因为这样做就打破了Java字符串不能修改的规定。但我们在开发当中,并不关心这个值是多少,通常情况下这个参数填NULL即可。

  2. 异常检查。
    调用完GetStringUTFChars之后不要忘记安全检查, if(c_str == NULL) { return NULL; }因为JVM需要为新诞生的字符串分配内存空间,当内存空间不够分配的时候,会导致调用失败,失败后GetStringUTFChars会返回NULL,并抛出一个OutOfMemoryError异常。JNI的异常和Java中的异常处理流程是不一样的,Java遇到异常如果没有捕获,程序会立即停止运行。而JNI遇到未决的异常不会改变程序的运行流程,也就是程序会继续往下走,这样后面针对这个字符串的所有操作都是非常危险的, 因此,我们需要用return语句跳过后面的代码,并立即结束当前方法。

  3. 调用ReleaseStringUTFChars释放字符串。
    在调用GetStringUTFChars函数从JVM内部获取一个字符串之后,JVM内部会分配一块新的内存,用于存储源字符串的拷贝,以便本地代码访问和修改。即然有内存分配,用完之后马上释放是一个编程的好习惯。通过调用ReleaseStringUTFChars函数通知JVM这块内存已经不使用了,你可以清除了。注意:这两个函数是配对使用的,用了GetXXX就必须调用ReleaseXXX。

  4. 调用NewStringUTF函数,创建字符串返回给java。
    通过调用NewStringUTF函数,会构建一个新的java.lang.String字符串对象。这个新创建的字符串会自动转换成Java支持的Unicode编码。如果JVM不能为构造java.lang.String分配足够的内存,NewStringUTF会抛出一个OutOfMemoryError异常,并返回NULL。在这个例子中我们不必检查它的返回值,如果NewStringUTF创建java.lang.String失败,OutOfMemoryError这个异常会被在Sample.main方法中抛出。如果NewStringUTF创建java.lang.String成功,则返回一个JNI引用,这个引用指向新创建的java.lang.String对象。

1.2 示例代码


JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello
  (JNIEnv *env, jclass cls, jstring j_str)
{
    const char *c_str = NULL;
    char buff[128] = {0};
    jboolean isCopy;    // 返回JNI_TRUE表示原字符串的拷贝,返回JNI_FALSE表示返回原字符串的指针
    c_str = (*env)->GetStringUTFChars(env, j_str, &isCopy);
    printf("isCopy:%d\n",isCopy);
    if(c_str == NULL)
    {
        return NULL;
    }
    printf("C_str: %s \n", c_str);
    sprintf(buff, "hello %s", c_str);
    (*env)->ReleaseStringUTFChars(env, j_str, c_str);
    return (*env)->NewStringUTF(env,buff);
}

1.3 其它字符串处理函数

1> GetStringChars和ReleaseStringChars:这对函数和Get/ReleaseStringUTFChars函数功能差不多,用于获取和释放以Unicode格式编码的字符串。后者是用于获取和释放UTF-8编码的字符串。

2> GetStringLength:由于UTF-8编码的字符串以'\0'结尾,而Unicode字符串不是。如果想获取一个指向Unicode编码的jstring字符串长度,在JNI中可通过这个函数获取。

3> GetStringUTFLength:获取UTF-8编码字符串的长度,也可以通过标准C函数strlen获取。

4> GetStringCritical和ReleaseStringCritical:提高JVM返回源字符串直接指针的可能性。

5> GetStringRegion和GetStringUTFRegion:分别表示获取Unicode和UTF-8编码字符串指定范围内的内容。这对函数会把源字符串复制到一个预先分配的缓冲区内。

2. C 方法中操作数组

NI中的数组分为基本类型数组和对象数组,它们的处理方式是不一样的,基本类型数组中的所有元素都是JNI的基本数据类型,可以直接访问。而对象数组中的所有元素是一个类的实例或其它数组的引用,和字符串操作一样,不能直接访问Java传递给JNI层的数组,必须选择合适的JNI函数来访问和设置Java层的数组对象。

2.1 操作基本类型数组

/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class com_study_jnilearn_IntArray */
 
#ifndef _Included_com_study_jnilearn_IntArray
#define _Included_com_study_jnilearn_IntArray
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_study_jnilearn_IntArray
 * Method:    sumArray
 * Signature: ([I)I
 */
JNIEXPORT jint JNICALL Java_com_study_jnilearn_IntArray_sumArray
  (JNIEnv *, jobject, jintArray);
 
#ifdef __cplusplus
}
#endif
#endif
 
// IntArray.c
#include "com_study_jnilearn_IntArray.h"
#include 
#include 
 
/*
 * Class:     com_study_jnilearn_IntArray
 * Method:    sumArray
 * Signature: ([I)I
 */
JNIEXPORT jint JNICALL Java_com_study_jnilearn_IntArray_sumArray
(JNIEnv *env, jobject obj, jintArray j_array)
{
    jint i, sum = 0;
    jint *c_array;
    jint arr_len;
    //1. 获取数组长度
    arr_len = (*env)->GetArrayLength(env,j_array);
    //2. 根据数组长度和数组元素的数据类型申请存放java数组元素的缓冲区
    c_array = (jint*)malloc(sizeof(jint) * arr_len);
    //3. 初始化缓冲区
    memset(c_array,0,sizeof(jint)*arr_len);
    printf("arr_len = %d ", arr_len);
    //4. 拷贝Java数组中的所有元素到缓冲区中
    (*env)->GetIntArrayRegion(env,j_array,0,arr_len,c_array);
    for (i = 0; i < arr_len; i++) {
        sum += c_array[i];  //5. 累加数组元素的和
    }
    free(c_array);  //6. 释放存储数组元素的缓冲区
    return sum;
}

2.2 操作引用数据类型数组

JNI提供了两个函数来访问对象数组,GetObjectArrayElement 返回数组中指定位置的元素,SetObjectArrayElement 修改数组中指定位置的元素。与基本类型不同的是,我们不能一次得到数据中的所有对象元素或者一次复制多个对象元素到缓冲区。因为字符串和数组都是引用类型,只能通过 Get/SetObjectArrayElement 这样的JNI函数来访问字符串数组或者数组中的数组元素。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class com_study_jnilearn_ObjectArray */
 
#ifndef _Included_com_study_jnilearn_ObjectArray
#define _Included_com_study_jnilearn_ObjectArray
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_study_jnilearn_ObjectArray
 * Method:    initInt2DArray
 * Signature: (I)[[I
 */
JNIEXPORT jobjectArray JNICALL Java_com_study_jnilearn_ObjectArray_initInt2DArray
  (JNIEnv *, jobject, jint);
 
#ifdef __cplusplus
}
#endif
#endif
 
 
// ObjectArray.c
 
#include "com_study_jnilearn_ObjectArray.h"
/*
 * Class:     com_study_jnilearn_ObjectArray
 * Method:    initInt2DArray
 * Signature: (I)[[I
 */
JNIEXPORT jobjectArray JNICALL Java_com_study_jnilearn_ObjectArray_initInt2DArray
  (JNIEnv *env, jobject obj, jint size)
{
    jobjectArray result;
    jclass clsIntArray;
    jint i,j;
    // 1.获得一个int型二维数组类的引用
    clsIntArray = (*env)->FindClass(env,"[I");
    if (clsIntArray == NULL)
    {
        return NULL;
    }
    // 2.创建一个数组对象(里面每个元素用clsIntArray表示)
    result = (*env)->NewObjectArray(env,size,clsIntArray,NULL);
    if (result == NULL)
    {
        return NULL;
    }
    
    // 3.为数组元素赋值
    for (i = 0; i < size; ++i)
    {
        jint buff[256];
        jintArray intArr = (*env)->NewIntArray(env,size);
        if (intArr == NULL)
        {
            return NULL;
        }
        for (j = 0; j < size; j++)
        {
            buff[j] = i + j;
        }
        (*env)->SetIntArrayRegion(env,intArr, 0,size,buff);
        (*env)->SetObjectArrayElement(env,result, i, intArr);
        (*env)->DeleteLocalRef(env,intArr);
    }
    
    return result;
}

3. 总结

对于引用类型(jObject),将参数转换或复制为本地类型,例如将jstring转换为C字符串,将jintArray转换为C的int []等;对于原始JNI类型,如jint和jdouble不需要转换,可以直接操作。

更多JNI&NDK系列文章,参见:
JNI&NDK开发最佳实践(一):开篇
JNI&NDK开发最佳实践(二):CMake实现调用已有C/C++文件中的本地方法
JNI&NDK开发最佳实践(三):CMake实现调用已有so库中的本地方法
JNI&NDK开发最佳实践(四):JNI数据类型及与Java数据类型的映射关系
JNI&NDK开发最佳实践(五):本地方法的静态注册与动态注册
JNI&NDK开发最佳实践(六):JNI实现本地方法时的数据类型转换
JNI&NDK开发最佳实践(七):JNI之本地方法与java互调
JNI&NDK开发最佳实践(八):JNI局部引用、全局引用和弱全局引用
JNI&NDK开发最佳实践(九):调试篇

你可能感兴趣的:(JNI&NDK开发最佳实践(六):JNI实现本地方法时的数据类型转换)