第5章


NDK开发常用的数据类型
及使用方法


  
  经过前4章的系统学习,读者应当已经能够熟练地创建并配置一个满足项目需求的NDK工程。在此基础上,本章将聚焦于NDK开发中的数据类型及其使用技巧。深入理解这些知识点,对于实现Java应用程序与原生代码之间的顺畅通信至关重要。
5.1  基础类型说明
  基本数据类型在JNI中可以直接与C/C++的基本数据类型相对应。为了实现这种映射关系,JNI使用typedef来定义这些基本类型之间的对应关系。Java与Native之间的数据类型映射关系见表5-1。
表5-1   基本数据类型
Java
JNI
C/C++
大小
boolean
jboolean
uint8_t
无符号8位
byte
jbyte
int8_t
有符号8位
char
jchar
uint16_t
无符号16位
short
jshort
int16_t
有符号16位
int
jint
int32_t
有符号32位
long
jlong
int64_t
有符号64位
float
jfloat
float
32位
double
jdouble
double
64位
     
  基本类型在jni.h文件中的定义,代码如下:
     
     //第5章/jni.h
     /* Primitive types that match up with Java equivalents. */
     typedef uint8_t  jboolean; /* unsigned 8 bits */
     typedef int8_t   jbyte;    /* signed 8 bits */
     typedef uint16_t jchar;    /* unsigned 16 bits */
     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 */
     
     /* "cardinal indices and sizes" */
     typedef jint     jsize;
     
5.2  引用类型说明
  关于引用类型,在Java语言中所有类都继承自java.lang.Object,然而,C语言并未引入类和对象的概念,并且Java类的内部结构在原生代码层面并未直接暴露,因此,在C语言中,普遍使用void*代替任意类型,它作为一个通用指针类型,能够指向任意类型的对象,而在C++中,为了模拟Java中的类,定义了一个空的类class Object {};,作为原生代码中的占位符,以此来与Java中的对象概念相对应。由于C和C++的实现并不相同,所以C和C++对应的实际类型是独立的,详细见表5-2。
表5-2?引用数据类型
Java
JNI
C
C++
java.lang.Class
jclass
jobject
_jclass *
java.lang.Throwable
jthrowable
jobject
_jthrowable*
java.lang.String
jstring
jobject
_jstring *
Other objects
jobject
void *
_jobject*
java.lang.Object[]
jobjectArray
jarray
_jobjectArray*
boolean[]
jbooleanArray
jarray
_jbooleanArray*
byte[]
jbyteArray
jarray
_jbyteArray*
char[]
jcharArray
jarray
_jcharArray*
short[]
jshortArray
jarray
_jshortArray*
int[]
jintArray
jarray
_jintArray*
long[]
jlongArray
jarray
_jlongArray*
float[]
jfloatArray
jarray
_jfloatArray*
double[]
jdoubleArray
jarray
_jdoubleArray*
Other arrays
Jarray
jarray
jarray*
     
   5.2.1  C语言下的引用类型
  C语言中对引用数据类型的定义,代码如下:
     
     /*
      * Reference types, in C.
      */
     typedef void*           jobject;
     typedef jobject         jclass;
     typedef jobject         jstring;
     typedef jobject         jarray;
     typedef jarray          jobjectArray;
     typedef jarray          jbooleanArray;
     typedef jarray          jbyteArray;
     typedef jarray          jcharArray;
     typedef jarray          jshortArray;
     typedef jarray          jintArray;
     typedef jarray          jlongArray;
     typedef jarray          jfloatArray;
     typedef jarray          jdoubleArray;
     typedef jobject         jthrowable;
     typedef jobject         jweak;
     
   5.2.2  C++语言下的引用类型
  C++语言中对引用数据类型的定义,代码如下:
     
     /*
      * Reference types, in C++
      */
     class _jobject {};
     class _jclass : public _jobject {};
     class _jstring : public _jobject {};
     class _jarray : public _jobject {};
     class _jobjectArray : public _jarray {};
     class _jbooleanArray : public _jarray {};
     class _jbyteArray : public _jarray {};
     class _jcharArray : public _jarray {};
     class _jshortArray : public _jarray {};
     class _jintArray : public _jarray {};
     class _jlongArray : public _jarray {};
     class _jfloatArray : public _jarray {};
     class _jdoubleArray : public _jarray {};
     class _jthrowable : public _jobject {};
     
     typedef _jobject*        jobject;
     typedef _jclass*         jclass;
     typedef _jstring*        jstring;
     typedef _jarray*         jarray;
     typedef _jobjectArray*  jobjectArray;
     typedef _jbooleanArray* jbooleanArray;
     typedef _jbyteArray*    jbyteArray;
     typedef _jcharArray*    jcharArray;
     typedef _jshortArray*   jshortArray;
     typedef _jintArray*     jintArray;
     typedef _jlongArray*    jlongArray;
     typedef _jfloatArray*   jfloatArray;
     typedef _jdoubleArray*  jdoubleArray;
     typedef _jthrowable*    jthrowable;
     typedef _jobject*        jweak;
     
  typedef 是一种为数据类型定义别名的机制,它有助于增强代码的可读性和可维护性。从上述代码来看,除了基本数据类型外,在引用类型的处理上,C语言最终将其视为void*,而C++则将其视为_jobject*。尽管全部统一使用void*或_jobject*作为表示是可行的,但分别使用不同的类型别名是为了提高代码的可读性和可扩展性。这样做有助于开发者更好地理解代码中指针的用途,以及它们所指向的对象类型,同时也为未来可能的类型扩展提供了更高的灵活性,因此,这种类型别名的使用方式不仅符合编程规范,也体现了良好的编程实践。
5.3  UTF-8和UTF-16字符串
  在讲解数据类型的操作函数之前,首先需要对字符集进行一番探讨。之所以要专门讲解字符集,是因为不同的编程语言和平台可能采用不同的字符集,这直接影响了字符类型在内存中的占用空间及字符的编码方式。
  特别地,我们注意到基本类型中的char在Java和C/C++中的表现是有所不同的。对于熟悉C语言的读者来讲,知道C语言中的char类型通常占用一字节的空间,用于存储ASCII字符集中的字符,然而,在Java语言中,char类型却占用了两字节的空间。同样地,JNI中定义的jchar类型也占用了两字节。
  这种差异的存在,主要是因为Java和C/C++使用了不同的字符集。Java采用的是Unicode字符集,这是一个能够涵盖世界上绝大多数语言和字符的宽字符集。由于Unicode字符集中包含了大量的字符,每个字符需要更多的空间来进行编码,因此Java中的char类型需要两字节来存储一个Unicode字符,而C语言则通常使用ASCII字符集,这是一个只包含基本拉丁字母和符号的较小字符集,因此其char类型只需一字节就能存储一个ASCII字符。
  因此,在使用jchar时,需要格外注意字符编码的问题。如果Java中的字符串仅包含ASCII字符,则可以直接将jchar转换为char使用,因为ASCII字符在Unicode中的表示与ASCII字符集中的表示是一致的,然而,如果Java字符串包含非ASCII字符(如汉字),则需要谨慎处理字符编码的转换问题。通常,可以使用Java中的String类的getBytes方法,将字符串转换为指定字符集(如UTF-8)的字节数组,然后在C/C++中利用相应的字符集函数将这些字节数组转换为char数组。
  综上所述,了解Java和C/C++在字符集处理上的差异,对于两者之间进行数据类型转换和通信至关重要。接下来,我们将探讨常用数据类型的操作函数,以便更高效地实现Java与原生代码之间的交互。
5.4  常用数据类型操作函数的使用
   5.4.1  String字符串的使用
  1. 字符串创建
  在原生代码(C或C++代码)中,可以使用NewString()函数来创建一个采用Unicode编码格式的字符串实例。使用NewStringUTF()函数则用于创建采用UTF-8编码格式的字符串实例。这两个函数都接受一个C语言风格的字符串(C字符串)作为参数,并返回一个Java字符串的引用类型,即jstring类型的值。
  使用C语言创建字符串实例,代码如下:
     
     //创建一个Unicode编码格式的字符串实例
     jstring str = (*env)->NewString(env, "hello world");
     //创建一个UTF-8编码格式的字符串实例
     jstring str = (*env)->NewStringUTF(env, "hello world");
     
  使用C++语言创建字符串实例,代码如下:
     
     //创建一个Unicode编码格式的字符串实例
     jstring str = env->NewString("hello world");
     //创建一个UTF-8编码格式的字符串实例
     jstring str = env->NewStringUTF("hello world");
     
  细心观察,使用C语言创建字符串和使用C++创建字符串略有不同,除C语言版本在参数上多了一个env参数外,还有env的使用方式不同。这主要取决于在C/C++中对于JNIEnv定义的不同。以一个普通的JNI函数代码举例,第1个参数为JNIEnv *env,代码如下:
     
     JNIEXPORT void JNICALL
     Java_com_example_javap_TestJni_test(JNIEnv *env, jobject thiz) {
     
     }
     
  在JNI的头文件jni.h中,JNIEnv分为C语言和C++语言定义两种版本,代码如下:
     
     //_JNIEnv定义
     struct _JNIEnv {
     const struct JNINativeInterface* functions;
     //…省略
     };
     
     #if defined(__cplusplus)     //C++中的实现
     typedef _JNIEnv JNIEnv;
     #else
     typedef const struct JNINativeInterface* JNIEnv;   //C语言中的实现
     #endif
     
  在C语言中,JNIEnv的定义实际上是指向struct JNINativeInterface的指针,即JNIEnv等同于struct JNINativeInterface*,因此,在C语言环境中,当我们看到一个JNIEnv* env作为函数参数时,env实际上是一个指向指针的指针,也就是一个双重指针。这意味着在使用env所指向的结构体中的函数或成员时,需要进行解引用操作。
  然而,在C++中,情况有所不同。在C++中,JNIEnv通常被定义为_JNIEnv结构的一个别名,而_JNIEnv结构内部包含了一个指向struct JNINativeInterface的指针,因此,在C++的上下文中,当JNIEnv* env作为参数传递时,env是一个指向_JNIEnv结构的指针,而该结构内部已经包含了指向JNINativeInterface的指针,所以,在C++中,可以直接使用指针操作。
  除了上述内容外,值得注意的是,C++对JNIEnv进行了封装处理。在其内部结构中,它保留了一个指向JNINativeInterface结构体的指针,因此,在C++中调用JNI接口函数时,JNINativeInterface会被直接作为函数的第1个参数传入,这就意味着在C++环境中调用JNI接口时,开发者无须再次显式地传入env作为函数的第1个参数,而在后续的讲解中,将更多地侧重于函数本身的定义及在C语言环境下的使用方式。
  2. 获取字符串UTFChars
  在原生代码(C或C++编写的代码)中,当需要与Java端的字符串进行交互时,可以使用GetStringUTFChars函数来获取Java字符串的UTF-8编码表示。该函数返回一个指向字节数组的指针,该字节数组表示采用修改后的UTF-8 编码的字符串。该数组在使用ReleaseStringUTFChars函数释放之前一直有效。
  接口定义,代码如下:
     
     /**
      * 获取字符串UTFChars
      * @param env       JNI接口指针
      * @param string    Java字符串对象
      * @param isCopy    指向布尔值的指针
      * @return 返回指向修改后的UTF-8字符串的指针,如果操作失败,则返回NULL
      */
     const char * GetStringUTFChars(JNIEnv *env, jstring string,jboolean *isCopy);
     
  isCopy是一个传出参数,在不为NULL的情况下,如果指针指向Java字符串的副本,则*isCopy会被设置为JNI_TRUE;如果*isCopy被赋值为JNI_FALSE,则说明直接指向Java字符串;如果不关心是否复制,则直接传入NULL即可。
  接口函数的使用,示例代码如下:
     
     jstring str;
     jboolean isCopy;
     const char *cString = (*env)->GetStringUTFChars(env, str, &isCopy);
       if (NULL == cString) {
           LOGE("获取C字符串失败\n");
       }
       if (isCopy == JNI_FALSE){
           LOGE("cString指向Java字符串");
       }else{
           LOGE("cString指向Java字符串的副本");
       }
     
  3. 释放字符串UTFChars
  使用GetStringUTFChars获取的字符串必须主动释放,否则会造成内存泄漏。
  接口定义,代码如下:
     
     /**
      * 释放字符串UTFChars
      * @param env    JNI接口指针
      * @param string Java字符串对象
      * @param utf    指向修改后的 UTF-8 字符串的指针
      */
     void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf)
     
  接口函数的使用,示例代码如下:
     
     jstring javaString;
     const char * cString;
     (*env)->ReleaseStringUTFChars(env, javaString, cString);
     
  4. 获取字符串长度
  使用GetStringLength()函数获取Java字符串长度,该函数有一个参数jstring,指向Java的字符串。返回字符串长度jsize(int)。
  接口定义,代码如下:
     
     /**
      * 获取字符串长度
      * @param env    JNI接口指针
      * @param string Java字符串对象
      * @return 返回 Java 字符串的长度
      */
     jsize GetStringLength(JNIEnv *env, jstring string);
     
  接口函数的使用,示例代码如下:
     
     jstring javaString;
     jsize length = (*env)->GetStringLength(env, javaString);
     
   5.4.2  数组操作
  JNI把Java数组当作引用类型来处理,和基本类型一样,JNI也提供了对Java数组进行处理的函数,以下对数组的操作以Int数组举例。
  1. 创建数组
  使用New<Type>Array()函数在原生代码中创建数组,其中<Type>可以是基本类型中的任意一种,例如NewIntArray()。该函数接受一个描述数组大小的参数,与NewString()函数一样,在失败时返回NULL。
  接口定义,代码如下:
     
     /**
      * 创建一个新的Java数组
      * @param env    JNI接口指针
      * @param length 数组长度
      * @return 返回一个 Java 数组,如果无法构造该数组,则返回NULL
      */
     ArrayType New<Type>Array(JNIEnv *env, jsize length);
     
  接口函数的使用,示例代码如下:
     
     //创建一个Java Int数组
     jintArray javaArray;
     javaArray = (*env)->NewIntArray(env, 10);
     if (NULL != javaArray){
         LOGE("可以操作数组了");
     }
     
  2. 获取Java数组
  JNI提供了两种访问Java数组元素的方法,可以将Java数组复制成C数组进行操作或者直接返回指向Java数组的指针。
  1)操作数组副本
  使用Get<Type>ArrayRegion()函数将给定的Java数组复制到给定的C数组中。
  接口函数的定义,代码如下:
     
     /**
      * 将给定的Java数组复制到给定的C数组中
      * @param env     JNI接口指针
      * @param array   Java数组
      * @param start   起始索引
      * @param len     要复制的元素数量
      * @param buf     目标缓冲区
      */
     void GetIntArrayRegion(JNIEnv *env, jintArray array, jsize start, jsize len, jint *buf)
     
  接口函数的使用,示例代码如下:
     
     //定义一个本地数组
     jint nativeArray[10];
     
     //从第0个元素开始,将10个Java数组中的元素复制到本地数组中
     (*env)->GetIntArrayRegion(env, javaArray, 0, 10, nativeArray);
     
  当使用Get<Type>ArrayRegion()函数时,需要确保提供的本地数组有效且足够大,以便存储从Java数组中提取的元素。如果start和len参数指定的范围超出了数组的实际大小,则JNI将抛出异常。复制成功后,原生代码就可以像使用普通的C数组一样使用和修改。
  当原生代码想将所作的修改提交给Java数组时,可以使用Set<Type>ArrayRegion()函数将C数组复制回Java数组,示例代码如下:
     
     jintArray javaArray;
     jint nativeArray[10];
     
     //从第0个元素开始,将10个Java数组中的元素复制到本地数组中
     (*env)->GetIntArrayRegion(env, javaArray, 0, 10, nativeArray);
      //数组操作
      for (int i = 0; i < 10; ++i) {
              nativeArray[i] = nativeArray[i] + 10;
           }
      //将修改后的数组提交到Java数组
     (*env)->SetIntArrayRegion(env, javaArray, 0, 10, nativeArray);
     
  2)操作数组指针
  使用Get<Type>ArrayElements()函数获取指向数组的元素的直接指针。在调用Release<Type>ArrayElements()函数之前,返回的指针一直有效。由于返回的数组可能是Java数组的副本,因此在调用Release<Type>ArrayElements()函数之前,对返回数组所做的更改不一定会反映到原始Java数组中。
  接口函数的定义,代码如下:
     
     /**
      * 获取指向数组的元素的指针
      * @param env               JNI接口指针
      * @param javaArray        Java数组
      * @param isCopy            指向布尔值的指针
      * @return   返回指向数组元素的指针,如果操作失败,则返回 NULL
      */
     jint* GetIntArrayElements(JNIEnv* env, jintArray javaArray, jboolean* isCopy);
     
     
     /**
      * 释放指向Java数组元素的直接指针
      * @param env     JNI接口指针
      * @param array   Java 数组对象
      * @param elems   指向数组元素的指针
      * @param mode    释放模式
      */void Release<Type>ArrayElements(JNIEnv *env, ArrayType array Type *elems, jint mode);
     
  接口函数的使用,示例代码如下:
     
     jboolean isCopy;
     jint * cArray = (*env)->GetIntArrayElements(env, javaArray, &isCopy);
     if (cArray == NULL){
         LOGE("java 数组获取失败");
         return ;
     }
     if (isCopy == JNI_TRUE){
         LOGE("数组指针指向Java数组副本");
     }else if (isCopy == JNI_FALSE){
         LOGE("Java 数组直接指向Java数组");
     }
     //释放指向Java数组元素的直接指针
     (*env)->ReleaseIntArrayElements(env, javaArray, cArray, 0);
     
  值得注意的是Release<Type>ArrayElements()函数的最后一个参数mode。该mode参数提供有关如何释放数组缓冲区的信息,如果elems不是数组中元素的副本,则mode无效,否则mode会有以下影响,详细见表5-3。
表5-3  数组释放模式
mode
影??响
0
将内容复制到Java数组并释放elems缓冲区
JNI_COMMIT
将内容复制到Java数组但不释放elems缓冲区
JNI_ABORT
释放缓冲区而不将可能的更改复制到Java缓冲区
     
  在大多数情况下,开发者将0传递给参数mode以确保固定数组和复制数组的行为一致。虽然其他选项可以使程序员更好地控制内存管理,但在使用时应格外小心。
     
注意:从JDK/JRE 1.1开始,程序员可以使用Get/Release<Type>ArrayElements()函数来获取指向原始数组元素的指针。如果VM支持pinning,则返回指向原始数据的指针,否则将制作一份副本。
     
  3. 获取Java端数组的直接指针
  从JDK/JRE 1.3 开始引入的新函数允许本机代码获取指向数组元素的直接指针,即使 VM 不支持pinning也是如此。
  接口函数的定义,代码如下:
     
     /**
      *  获取指向数组的元素的直接指针
      * @param env     JNI接口指针