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;
}