背景
随着Android项目中c++代码部分功能复杂程度的增加,jni中需要传递的数据类型也越来越多,关于jni数据类型转换网上有不少相关文章,但是在使用时发现这些例子中存在不少谬误,遂在此重新总结相关内容,并附相关例程,以便日后参考。
下文我们将对以下几种常见情况进行分析,其中前4种是java向native传递数据,后3种是native向java返回数据,分别列举如下:
- java向native传递常用基本数据类型 和字符串类型
- java向native传递数组类型
- java向native传递自定义java对象
- java向native传递任意java对象(以向native传递ArrayList为例)
- native向java传递数组类型
- native向java传递字符串类型
- native向java传递java对象
例程
此处先介绍一下后面例子中使用的jni包装类,该类提供了上述7种常用方法的java封装,代码如下
/**
* Created by lidechen on 1/23/17.
*/
public class JNIWrapper {
// java向native传递常用基本数据类型 和字符串类型
public native void setInt(int data);
public native void setLong(long data);
public native void setFloat(float data);
public native void setDouble(double data);
public native void setString(String data);
//java向native传递任意java对象(以向native传递ArrayList为例)
public native void setList(List list, int len);
//java向native传递自定义java对象
public native void setClass(Package data);
//java向native传递数组类型
public native void setBuf(byte[] buf, int len);
//native向java传递字符串类型
public native String getString();
//native向java传递数组类型
public native byte[] getBuf();
//native向java传递java对象
public native Package getPackage();
public static class Package{
public boolean booleanData;
public byte byteData;
private int intData;
public long longData;
public float floatData;
public double doubleData;
public String stringData;
public byte[] byteArray;
public List<String> list;
public void setIntData(int data){
intData = data;
}
public int getIntData(){
return intData;
}
}
}
1.java向native传递常用基本数据类型 和字符串类型
java层
//传递常用基本类型
wrapper.setInt(123);
wrapper.setLong(123L);
wrapper.setFloat(0.618f);
wrapper.setDouble(0.618);
wrapper.setString("hello");
对应native层
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setInt
* Signature: (I)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setInt
(JNIEnv *env, jobject obj , jint data){
LOGE("setInt %d", data);
}
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setLong
* Signature: (J)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setLong
(JNIEnv *env, jobject obj, jlong data){
LOGE("setLong %ld", data);
}
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setFloat
* Signature: (F)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setFloat
(JNIEnv *env, jobject obj, jfloat data){
LOGE("setLong %f", data);
}
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setDouble
* Signature: (D)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setDouble
(JNIEnv *env, jobject obj, jdouble data){
LOGE("setDouble %lf", data);
}
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setString
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setString
(JNIEnv *env, jobject obj, jstring jdata){
const char* cdata = env->GetStringUTFChars(jdata, 0);
LOGE("setString %s", cdata);
env->ReleaseStringUTFChars(jdata, cdata);
env->DeleteLocalRef(jdata);
}
上述代码中,除了引用类型String外,其他基本类型是直接可以拿来就用的,而且也不需要进行额外的回收操作。可以看一下jni.h中对于基本类型的定义:
jni.h中定义
#ifdef HAVE_INTTYPES_H
# include <inttypes.h> /* C99 */
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 */
#else
typedef unsigned char jboolean; /* unsigned 8 bits */
typedef signed char jbyte; /* signed 8 bits */
typedef unsigned short jchar; /* unsigned 16 bits */
typedef short jshort; /* signed 16 bits */
typedef int jint; /* signed 32 bits */
typedef long long jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
#endif
可见, 这些j开头的基本类型其实和c的基本类型是等价的,我们直接使用即可。
需要注意的是String类型,它是一个java的引用类型,这里我们需要使用env的方法GetStringUTFChars将java的String类型转换成c++中的const char*类型,相当于是一个字符串常量,我们只能读取这个字符串的信息。另外在使用完毕后我们需要释放这个String类型对象的资源,这点不同于基本类型可以直接不管。
2. java向native传递数组类型
java向native传递数组类型比较常见,比如我们经常会在java中获取一些音频或者图像数据,然后传递到native层,使用c++编写的算法对这些数据进行处理,而且这类代码往往传送的数据量比较大,如果没有正确释放很可能会吃尽所有系统内存。
这里也要顺便说明一点,我们在native中开辟的堆内存是不受android虚拟机内存限制的,可以通过在jni中malloc一块大于当前虚拟机限制的内存来验证。
下面是java层向native层传入数组的例子
//传递数组
byte[] buf = new byte[5];
buf[0] = 49;
buf[1] = 50;
buf[2] = 51;
buf[3] = 52;
buf[4] = 53;
wrapper.setBuf(buf, 5);
native层接收数据
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setBuf
* Signature: ([B)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setBuf
(JNIEnv *env, jobject obj, jbyteArray jbuf, jint len){
jbyte *cbuf = env->GetByteArrayElements(jbuf, JNI_FALSE);
for(int i=0; i<len; i++){
LOGE("setBuf buf[%d] %c", i, cbuf[i]);
}
env->ReleaseByteArrayElements(jbuf, cbuf, 0);
env->DeleteLocalRef(jbuf);
}
这里还是调用方法先将java的数组转换为c的指针类型,这样就能拿到数组中的元素的了。与上述String一样,使用完数组后我们必须将其释放。
可见,java传入一般类型的数据,到了native层首先是要将其转换成c中对应的类型,然后使用c的方式对数据进行操作,最后再释放资源。
上述的几种类型jni.h中已经给我们提供了相应的转换函数以及对应的转换类型,但是对于自定义的数据类型或者java自带的其它类型如何传递给native层呢?
3. java向native传递自定义java对象
传递自定义的java对象在开发中是很常见的,比如一个算法需要接受很多个参数,如果直接写到函数jni的参数中,那么如果还要增加或者减少参数数量或者改变类型时必然需要重新生成jni接口。这时我会将这些参数封装为一个类,定义一个对象将其一起传递给native层。
这里我们传递自定义的类型Package,定义在JNIWrapper中。
java层构造并传入对象
//传递自定义java对象 并在jni中获取java对象的属性值
JNIWrapper.Package pkg = new JNIWrapper.Package();
pkg.booleanData = true;
//pkg.intData = 12345;
//注意 int 参数是一个私有属性,在jni中也可以直接拿到
pkg.setIntData(12345);
pkg.longData = 12345L;
pkg.floatData = 3.14159f;
pkg.doubleData = 3.14159;
pkg.stringData = "hello class";
pkg.byteArray = buf;
List<String> list2 = new ArrayList<String>();
list2.add("str 1");
list2.add("str 2");
list2.add("str 3");
pkg.list = list2;
wrapper.setClass(pkg);
native层接收对象
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setClass
* Signature: (Lcom/vonchenchen/myapplication/JNIWrapper/Package;)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setClass
(JNIEnv *env, jobject obj, jobject data){
jclass cdata = env->GetObjectClass(data);
//boolean 比较特殊
jfieldID booleanDataID = env->GetFieldID(cdata, "booleanData", "Z");
jboolean cbooleanData = env->GetBooleanField(data, booleanDataID);
jfieldID byteDataID = env->GetFieldID(cdata, "byteData", "B");
jboolean cbyteData = env->GetByteField(data, byteDataID);
//注意JAVA 对象的私有属性此处也可以获取到
jfieldID intDataID = env->GetFieldID(cdata, "intData", "I");
jint cintData = env->GetIntField(data, intDataID);
//long比较特殊
jfieldID longDataID = env->GetFieldID(cdata, "longData", "J");
jlong clongData = env->GetLongField(data, longDataID);
jfieldID floatDataID = env->GetFieldID(cdata, "floatData", "F");
jfloat cfloatData = env->GetFloatField(data, floatDataID);
//
jfieldID doubleDataID = env->GetFieldID(cdata, "doubleData", "D");
jdouble cdoubleData = env->GetDoubleField(data, doubleDataID);
jfieldID stringDataID = env->GetFieldID(cdata, "stringData", "Ljava/lang/String;");
jstring cstringData = (jstring)env->GetObjectField(data, stringDataID);
const char *ccharData = env->GetStringUTFChars(cstringData, JNI_FALSE);
//
LOGE("setClass bool %d", cbooleanData);
LOGE("setClass byte %d", cbyteData);
LOGE("setClass int %d", cintData);
LOGE("setClass long %ld", clongData);
LOGE("setClass float %f", cfloatData);
LOGE("setClass double %lf", cdoubleData);
LOGE("setClass String %s", ccharData);
env->ReleaseStringUTFChars(cstringData, ccharData);
env->DeleteLocalRef(cstringData);
}
jni并不知道我们传入数据的类型,也就没办法拿到这个对象的属性或者操作这个对象的方法。所以第一步先是通过调用GetObjectClass方法,得到我们传入对象的类。
有了这个类之后,我们就可以用GetxxxField方法和GetMethodID方法分别拿到对象的属性和方法ID。 有了方法或者属性id,我们就可以从传入的jobject对象中获取这个属性或者方法并执行了。
注意,我们在获取属性时需要知道这个属性的签名,那么签名如何拿到呢?
如何获取属性和方法的签名
现在以我们的Package为例,来看一下获取签名的流程。
这里我们使用javap命令,来对要查看类的.class文件进行操作。那么我们就先找到.class文件的存放位置。下图是Android Studio生成.class的文件位置。
这里找到了JNIWrapper的class文件,而Package是JNIWrapper的内部类。执行如下命令
上图注意执行命令的目录层级,应该在debug文件目录下执行指令,具体路径见截图,大家需要根据自己的工程进行设置。
javap -s 完整类名
如果需要哪个类型的函数或者属性签名,直接找对应项的descriptor即可。
4. java向native传递任意java对象(以向native传递ArrayList为例)
现在我们把ArrayList传给native层,让native层拿到ArrayList中存储的数据。
java层传递ArrayList给native层
//传入一般类型的java对象 并在jni中调用java方法 此处以ArrayList为例
List<Integer> list = new ArrayList<Integer>();
list.add(111);
list.add(222);
list.add(333);
wrapper.setList(list, 3);
native层接收
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setList
* Signature: (Ljava/util/List;)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setList
(JNIEnv *env, jobject obj, jobject data, jint len){
//传入一个JAVA 的ArrayList对象,存放的范型为Integer,下面我们尝试拿到ArrayList的第一个元素
//获取传入对象的java类型,也就是ArrayList
jclass datalistcls = env->GetObjectClass(data);
//执行 javap -s java.util.ArrayList 查看ArrayList的函数签名
/* public E get(int);
descriptor: (I)Ljava/lang/Object;
*/
//从ArrayList对象中拿到其get方法的方法ID
jmethodID getMethodID = env->GetMethodID(datalistcls, "get", "(I)Ljava/lang/Object;");
//调用get方法,拿到list中存储的第一个Integer 对象
jobject data0 = env->CallObjectMethod(data, getMethodID, 0);
//javap -s java/lang/Integer
jclass datacls = env->GetObjectClass(data0);
/*
* public int intValue();
descriptor: ()I
*/
jmethodID intValueMethodID = env->GetMethodID(datacls, "intValue", "()I");
//将Integer 对象的int值取出
int data0_int = env->CallIntMethod(data0, intValueMethodID);
LOGE("setList buf[0] %d", data0_int);
}
这里先拿到传入jobject的真正的类,然后调用get方法拿到了ArrayList中存储的范型元素,由于我们传入的是Integer 类型,我们在从Integer中拿到包装的int数据,具体过程参考注释。
5. native向java传递数组类型
现在开始我们来看native如何返给java数据。现在我们在native中生成一个c数组,我们将数组数据返回给java。
native方法
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: getBuf
* Signature: ()[B
*/
JNIEXPORT jbyteArray JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_getBuf
(JNIEnv *env, jobject obj){
//char *buf = "I am from jni";
char buf[] = "getBuf : I am from jni";
int len = sizeof(buf);
LOGE("sizeof %d", len); // 注意sizeof对于数组和指针是区别对待的
jbyteArray ret = env->NewByteArray(len);
env->SetByteArrayRegion(ret, 0, len, (jbyte *) buf);
return ret;
}
这里首先创建并初始化了一个c数组,然后在native层调用NewByteArray方法生成一个java数组,再使用SetByteArrayRegion将c数组的值复制到java数组中。
6. native向java传递字符串类型
native 层
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: getString
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_getString
(JNIEnv *env, jobject obj){
const char buf[] = "getString : I am from jni";
return env->NewStringUTF(buf);
}
调用NewStringUTF生成一个java的string,然后返回即可。
7. native向java传递java对象
这里我们可以直接在native中生成一个java对象,并且对其属性赋值,最终将构造好的对象直接返回给java层。
native 代码
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: getPackage
* Signature: ()Lcom/vonchenchen/myapplication/JNIWrapper/Package;
*/
JNIEXPORT jobject JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_getPackage
(JNIEnv *env, jobject obj){
//获取类对象 这个class文件存在于dex中,我们可以通过分析apk工具查看
jclass packagecls = env->FindClass("com/vonchenchen/myapplication/JNIWrapper$Package");
//获取这个类的构造方法的方法id 以及这个方法的函数签名
jmethodID construcMethodID = env->GetMethodID(packagecls, "<init>", "()V");
//创建这个java对象
jobject packageobj = env->NewObject(packagecls, construcMethodID);
//操作对象的属性
jfieldID intDataID = env->GetFieldID(packagecls, "intData", "I");
env->SetIntField(packageobj, intDataID, 88888);
return packageobj;
}
这里需要注意的是使用FindClass方法找到要生成的类。Package为内部类,使用$作为分割符号。这个方法会在apk中的dex包中找到对应的class文件并进行加载。
之后获取构造方法,使用构造方法生成对应的对象。有了对象就可以使用属性或者方法id操作对应的属性和方法了。
完整例程下载地址:http://download.youkuaiyun.com/detail/lidec/9747426