JNI系列(二) —— 数据类型

JNI系列(二) —— 数据类型

java 的基本数据类型在JNI层的映射

既然 java 需要再jni层面进行处理,jni底层又是 c/c++ 那么必然存在 java 的数据类型与 c/c++ 的数据类型的映射,在jni层中处理,数据必然是要统一的,比如说调用native方法的时候,传入的是一个java层面的参数,传到jni中时,jni该如何"看待"这个参数,以及在jni层返回一个数据到java层,也一定要返回一个java层面"认识"的一个数据。

JNI 对于 Java 的基础数据类型(int 等)和引用数据类型(Object、Class、数组等)的处理方式不同。这个原理非常重要,理解这个原理才能理解后面所有 JNI 函数的设计思路:

  • 基础数据类型: 会直接转换为 C/C++ 的基础数据类型,例如 int 类型映射为 jint 类型。由于 jint 是 C/C++ 类型,所以可以直接当作普通 C/C++ 变量使用,而不需要依赖 JNIEnv 环境对象;

  • 引用数据类型: 对象只会转换为一个 C/C++ 指针,例如 Object 类型映射为 jobject 类型。由于指针指向 Java 虚拟机内部的数据结构,所以不可能直接在 C/C++ 代码中操作对象,而是需要依赖 JNIEnv 环境对象。另外,为了避免对象在使用时突然被回收,在本地方法返回前,虚拟机会固定(pin)对象,阻止其 GC。

另外需要特别注意一点,基础数据类型在映射时是直接映射,而不会发生数据格式转换。例如,Java char 类型在映射为 jchar 后旧是保持 Java 层的样子,数据长度依旧是 2 个字节,而字符编码依旧是 UNT-16 编码。

具体映射关系都定义在 jni.h 头文件中,文件摘要如下:

typedef uint8_t  jboolean; /* unsigned 8 bits */
typedef int8_t   jbyte;    /* signed 8 bits */
typedef uint16_t jchar;    /* unsigned 16 bits */ /* 注意:jchar 是 2 个字节 */
typedef int16_t  jshort;   /* signed 16 bits */
typedef int32_t  jint;     /* signed 32 bits */
typedef int64_t  jlong;    /* signed 64 bits */
typedef float    jfloat;   /* 32-bit IEEE 754 */
typedef double   jdouble;  /* 64-bit IEEE 754 */
typedef jint     jsize;

#ifdef __cplusplus
// 内部的数据结构由虚拟机实现,只能从虚拟机源码看
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
...
// 说明我们接触到到 jobject、jclass 其实是一个指针
typedef _jobject*       jobject;
typedef _jclass*        jclass;
typedef _jstring*       jstring;
typedef _jarray*        jarray;
typedef _jobjectArray*  jobjectArray;
typedef _jbooleanArray* jbooleanArray;
...
#else /* not __cplusplus */
...
#endif /* not __cplusplus */

Java 类型与 JNI 类型的映射关系总结为下表:

Java 类型 JNI 类型 描述 长度(字节)
boolean jboolean unsigned char 1
byte jbyte signed char 1
char jchar unsigned short 2
short jshort signed short 2
int jint、jsize signed int 4
long jlong signed long 8
float jfloat signed float 4
double jdouble signed double 8
Class jclass Class 类对象 1
String jstrting 字符串对象 /
Object jobject 对象 /
Throwable jthrowable 异常对象 /
boolean[] jbooleanArray 布尔数组 /
byte[] jbyteArray byte 数组 /
char[] jcharArray char 数组 /
short[] jshortArray short 数组 /
int[] jinitArray int 数组 /
long[] jlongArray long 数组 /
float[] jfloatArray float 数组 /
double[] jdoubleArray double 数组 /

字符串类型操作

上面提到 Java 对象会映射为一个 jobject 指针,那么 Java 中的 java.lang.String 字符串类型也会映射为一个 jobject 指针。可能是因为字符串的使用频率实在是太高了,所以 JNI 规范还专门定义了一个 jobject 的派生类 jstring 来表示 Java String 类型,这个相对特殊。

jni.h文件

// 内部的数据结构还是看不到,由虚拟机实现
class _jstring : public _jobject {};
typedef _jstring*       jstring;

struct JNINativeInterface {
    // String 转换为 UTF-8 字符串
    const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
    // 释放 GetStringUTFChars 生成的 UTF-8 字符串
    void        (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);
     // String 转换为 UTF-8 字符串
    const char* (*GetStringChars)(JNIEnv*, jstring, jboolean*);
    // 释放 GetStringUTFChars 生成的 UTF-8 字符串
    void        (*ReleaseStringChars)(JNIEnv*, jstring, const char*);
    
    // 构造新的 String 字符串
    jstring     (*NewStringUTF)(JNIEnv*, const char*);
    // 获取 String 字符串的长度
    jsize       (*GetStringUTFLength)(JNIEnv*, jstring);
    // 将 String 复制到预分配的 char* 数组中
    void        (*GetStringUTFRegion)(JNIEnv*, jstring, jsize, jsize, char*);
};
复制代码

由于 Java 与 C/C++ 默认使用不同的字符编码,java中使用的是UTF-16,因此在操作字符数据时,需要特别注意在 UTF-16 和 UTF-8 两种编码之间转换。

对上面的函数详细说明下:

const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
// 1. 获得的字符串是通过UTF-8进行编码的
// 2. 注意因为java层面默认的是UTF-16编码,所以是先将UTF-16转换成UTF-8
// 3. 第三个参数决定是使用拷贝模式还是复用模式
	| // JNI_TRUE: 使用拷贝模式,JVM 将拷贝一份原始数据来生成 UTF-8 字符串;
 	| // JNI_FALSE: 使用复用模式,JVM 将复用同一份原始数据来生成 UTF-8 字符串。复用模式		| //  绝不能修改字符串内容,否则 JVM 中的原始字符串也会被修改,打破 String 不可变性。
 // 4. 必须ReleaseStringUTFChars进行释放
  • java -> jni字符串的情形:

java使用默认的UTF-16编码的字符串,jvm将这个字符串传给jni, jni得到的输入是jstring,这个时 候,可以利用jni提供的两种函数,一个是GetStringUTFChars,这个函数将得到一个UTF-8编码的字符串;另一个是 GetStringChars这个将得到UTF-16编码的字符串的地址。

/******************* JNI ******************/
#include <iostream>
#include <iomanip>
#include "myJni.h"

using namespace std;


void printHex(const char* ch)
{
	int len = 0;
	while (*(ch + len++) != '\0');	// 获取字符个数
	
	for (int i = 0; i < len - 1; i++)	// len - 1 去掉 结束符
	{	
        // 大写的十六进制显示
		cout << hex << setiosflags(ios::uppercase) << (int(*(ch + i) & 0xff));
	}
	cout << endl;
}


JNIEXPORT jstring JNICALL Java_com_tx_JniTest_test1(JNIEnv *env, jclass, jstring str)
{
	const char* str_UTF8 = env->GetStringUTFChars(str, JNI_FALSE);
	printHex(str_UTF8);		// 打印结果 E4BDA0
    // 需要释放资源
	env->ReleaseStringUTFChars(str, str_UTF8);
	return NULL;
}

结果打印出来: E4BDA0 这就是 "你" 的 UTF-8 编码

而对应的 GetStringChars 如下:

// JNI 
JNIEXPORT jstring JNICALL Java_com_tx_JniTest_test1(JNIEnv *env, jclass, jstring str)
{
	const jchar* str_UTF16 = env->GetStringChars(str, JNI_FALSE);

	cout << hex << setiosflags(ios::uppercase) << *str_UTF16 << endl;	// 4F60

	env->ReleaseStringChars(str, str_UTF16);
	return NULL;
}

结果输出: 4F60 这是 "你" 的UTF-16的编码。

注意到不论是GetStringUTFChars还是GetStringChars 最后都是要进行ReleaseStringUTFChars或者ReleaseStringChars。最开始的时候我不理解为什么参数里面为啥要填入jstring参数,后来看到资料,推测可能是因为获得字符串的过程有关。

以 GetStringUTFChars(jstring, jboolean*)为例, 如果第二个参数是NULL或者JNI_FALSE(就是0和NULL同值)时,就是复用原来的数据,先在jni层面分配一个空间用来存储要转换过来的UTF-8,并且形成这个空间到java层面UTF-16字符串的映射,这样java层面的UTF-16字符串先转换成UTF-8然后存储到jni开辟的空间中。因为形成映射,相当于给java层面的字符串打了个标记,放置GC回收,那么当jni使用完后,当然需要释放这个字符串空间(jni层面开辟用于存储UTF8的空间),以及将那个标记取消掉,java层面好GC,因此,需要ReleaseStringUTFChars两个参数。

不过,我有个不理解的地方,网上很多人说在复用java层字符串的时候,不要修改JNI层面的字符串,否者会改变java层面的字符串(可能是因为那个映射关系,jni层面变动会自动更新java层面的数据),这与String的不可变性想冲突。

const char* str_UTF8 = env->GetStringUTFChars(str, JNI_FALSE);

可是可以看到获取的指针是个cost修饰的,JNI层面根本修改不了。如何更新java层面的String?

另外还有一个基于范围的转换函数:GetStringUTFRegion:预分配一块字符数组缓冲区,然后将 String 数据复制到这块缓冲区中。由于这个函数本身不会做任何内存分配,所以不需要调用对应的释放资源函数,也不会抛出 OutOfMemoryError。另外,GetStringUTFRegion 这个函数会做越界检查并抛出 StringIndexOutOfBoundsException 异常。

示例程序:

// JNI 部分

void printHex(const char* ch)
{
	int len = 0;
	while (*(ch + len++) != '\0');
	for (int i = 0; i < len - 1; i++)
	{
		cout << hex << setiosflags(ios::uppercase) << (int(*(ch + i) & 0xff));
	}
	cout << endl;
}


JNIEXPORT jstring JNICALL Java_com_tx_JniTest_test1(JNIEnv *env, jclass, jstring str)
{
	 char ch[20];
	 // 获取的是字符个数
	 cout << env->GetStringLength(str) << endl;
	 env->GetStringUTFRegion(str, 0, env->GetStringLength(str), ch);
	 printHex(ch);	// E4BDA0  是 "你"  的 UTF-8的编码
	
	return NULL;
}

注意: 这种方式不需要release释放空间,但前提是你得提供装字符串的字符数组。

数组类型操作

基础数据类型的数组操作(以jintArray为例)

主要操作有:

  • 创建java层面的数组 env->newIntArray。
  • 将java层面的数组转换成jni层面的数组 env->GetIntArrayElements
  • 释放jni层面的数组,以及同步更新到java层面的数组 env->ReleaseIntArrayElements
  • 区域复制java层面数组到JNI层面 env->GetIntArrayRegion

ReleaseIntArrayElements 函数的第 3 个参数 mode 做解释:它是一个模式参数:

参数 mode 描述
0 将 C/C++ 数组的数据回写到 Java 数组,并释放 C/C++ 数组
JNI_COMMIT 将 C/C++ 数组的数据回写到 Java 数组,并不释放 C/C++ 数组
JNI_ABORT 不回写数据,但释放 C/C++ 数组

对比字符串的操作, env->GetxxxArrayElements 和 env->GetStringUTFChars 相似,env->GetxxxArrayRegion 与 env->GetStringUTFRegion 相似

示例代码:

   	// java 部分
	// 测试int[]
    public static native int[] test2();


    public static void main(String[] args)   {
        int[] ints = test2();
        for (int i : ints) {
            System.out.print(i + " ");	// 0 10 20 30 40 50 60 70 80 90 
        }

    }
	// jni 部分
JNIEXPORT jintArray JNICALL Java_com_tx_JniTest_test2
	(JNIEnv *env, jclass, jintArray jint_Arr)
{
	// 创建 java 层面的 int 数组
	jintArray jIArr = env->NewIntArray(10);
	// 创建jni层面jint数组,并与java层面的数组形成关联
	jint* p_jint = env->GetIntArrayElements(jIArr, JNI_FALSE);
	// 修改 jni 层面数组
	for (int i = 0; i < 10; ++i)
	{
		p_jint[i] = i * 10;
	}
	// 同步更新到java层面, 并释放jni层面的数组,同时取消标记
	env->ReleaseIntArrayElements(jIArr, p_jint, 0);
	return jIArr;

}

建议用 env->GetIntArrayRegion, 不过需要事先分配空间。

引用类型数组操作(jobjectArray)

主要操作有:

  • 构造一个java 层面的对象数组 env->NewObjectArray
  • 对数组特定下标进行赋值 env->SetObjectArrayElement

直接看示例:

    
	// java 代码
	public static native String[] test3();

    public static void main(String[] args)   {
        String[] strArr = test3();
        for (String s : strArr) {
            System.out.print(s + " ");	// 0 1 2 3 4 5 6 7 8 9 
        }
    }
	// jni 代码
    JNIEXPORT jobjectArray JNICALL Java_com_tx_JniTest_test3
        (JNIEnv *env, jclass)
    {
        // 创建 JAVA 层面的String[]
        jobjectArray jobj_Arr = env->NewObjectArray(10, env->FindClass("Ljava/lang/String;"), NULL);
        jstring jstr;
        char ch[10];
        for (int i = 0; i < 10; ++i)
        {
            sprintf_s(ch, "%d", i);
            jstr = env->NewStringUTF(ch);
            env->SetObjectArrayElement(jobj_Arr, i, jstr);
        }
        return jobj_Arr;
    }
posted @ 2023-04-25 11:40  野生程序猿_芯  阅读(337)  评论(0)    收藏  举报