Androidjnindk编程⼆:jni数据类型转换(primitive,String,a。。。
⼀.数据类型映射概述
从我们开始jni编程起,就不可能避开函数的参数与返回值的问题。java语⾔的数据类型和c/c++有很多不同的地⽅,所以我们必须考虑当在java层调⽤c/c++函数时,怎么正确的把java的参数传给c/c++函数,怎么正确的从c/c++函数获取正确的函数返回值;反之,当我们在c/c++中使⽤java的⽅法或属性时,如何确保数据类型能正确的在java和c/c++之间转换。
回顾我们上⼀篇⽂章中的那个c函数:
#include <stdio.h>
#include <jni.h>
#include <stdlib.h>
JNIEXPORT jstring JNICALL Java_com_jinwei_jnitesthello_MainActivity_sayHello(JNIEnv * env, jobject obj){
return (*env)->NewStringUTF(env,"jni say hello to you");
}
这个函数⾮常简单,它没有接受参数,但是它返回了⼀个字符串给java层。我们不能简单的使⽤return “jni say hello to you”;⽽是使⽤了NewStringUTF函数做了个转换,这就是数据类型的映射。
普通的jni函数⼀般都会有两个参数:JNIEnv * env, jobject obj,第三个参数起才是该函数要接受的参数,所以这⾥说它没有接受参数。
1.1JNIEnv * env
JNIEnv是⼀个线程相关的结构体, 该结构体代表了 Java 在本线程的运⾏环境。这意味不同的线程各⾃拥有各⼀个JNIEnv结构体,且彼此之间互相独⽴,互不⼲扰。NIEnv结构包括了JNI函数表,这个函数表中存放了⼤量的函数指针,每⼀个函数指针⼜指向了具体的函数实现,⽐如,例⼦中的NewStringUTF函数就是这个函数表中的⼀员。
JVM,JNIEnv与native function的关系可⽤如下图来表述:
1.2 jobject obj
这个参数的意义取决于该⽅法是静态还是实例⽅法(static or an instance method)。
当本地⽅法作为⼀个实例⽅法时,第⼆个参数相当于对象本⾝,即this. 当本地⽅法作为
⼀个静态⽅法时,指向所在类. 在本例中,sayHello⽅法是实例⽅法,所以obj就相当于this指针。
⼆.基本数据类型的映射
在Java中有两类数据类型:primitive types,如,int, float, char;另⼀种为
reference types,如,类,实例,数组。
java基本类型与c/c++基本类型可以直接对应,对应⽅式由jni规范定义:
JNI基本数据类型的定义在jni.h中:
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 */
也就是说jni.h中定义的数据类型已经是c/c++数据类型了,我们使⽤jint等类型的时候要明⽩其实使⽤的就是int 数据类型。
2.1实践
我们在上⼀篇博客中实现的程序的基础上进⼀步实验,现在给native_sayHello函数添加⼀个参数,c代码如下:
#include <stdio.h>
#include <jni.h>
#include <stdlib.h>
jstring native_sayHello(JNIEnv * env, jobject obj,jint num){
char array[30];
snprintf(array,30,"jni accept num : %d",num);
return (*env)->NewStringUTF(env,array);
}
static JNINativeMethod gMethods[] = {
{"sayHello", "(I)Ljava/lang/String;", (void *)native_sayHello},
};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv* env = NULL; //注册时在JNIEnv中实现的,所以必须⾸先获取它
jint result = -1;
if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_4) != JNI_OK) //从JavaVM获取JNIEnv,⼀般使⽤1.4的版本
return -1;
jclass clazz;
static const char* const kClassName="com/jinwei/jnitesthello/MainActivity";
clazz = (*env)->FindClass(env, kClassName); //这⾥可以到要注册的类,前提是这个类已经加载到java虚拟机中。这⾥说明,动态库和有native⽅法的类之间,没有任何对应关系。
if(clazz == NULL)
{
printf("cannot get class:%s\n", kClassName);
return -1;
}
if((*env)->RegisterNatives(env,clazz,gMethods, sizeof(gMethods)/sizeof(gMethods[0]))!= JNI_OK) //这⾥就是关键了,把本地函数和⼀个java类⽅法关联起来。不管之前是否关联过,⼀律把之前的替换掉! {
printf("register native method failed!\n");
return -1;
}
return JNI_VERSION_1_4;
}
这⾥还是使⽤动态的⽅式注册本地⽅法,相⽐较之前的代码,这⾥只做了两处修改:
1.native_sayHello增加了⼀个参数jint num;
2.函数签名也随之改变: “(I)Ljava/lang/String;”,之前是 “()Ljava/lang/String;”
层的代码也随之改变:
@Override
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
textView = (TextView) findViewById();
String hehe = this.sayHello(12345);
textView.setText(hehe);
}
public native String sayHello(int num);
这样就会在TextView中显⽰出jni accept num : 12345。
这个实验验证了java基本类型可以直接对应到c/c++的基本类型。
三.字符串的转换
java的String与c/c++的字符串有很⼤不同,⼆者之间不能直接对应,其转换需要通过jni函数来实现。
jni⽀持Unicode和utf-8两种编码格式的转换。Unicode代表的了16-bit字符集,utf-8则兼容ASCII码,java虚拟机使⽤的Unicode编码,c/c++则默认使⽤ASCII码。这因为jni ⽀持Unicode和utfbain吗之间的转换,所以我们可以使⽤Jni规范提供的函数在java与c/c++之间转换数据类型。
这⼀节我们从字符串说起,jni使⽤的字符串类型是jstring,我们先看看它的定义:
c++中:
class _jobject {};
jsp语言简介class _jstring : public _jobject {};
typedef _jstring* jstring;
可见在c++中jstring是_jsting*类型的指针,_jstring是⼀个继承了_jobject类的类。
c中:
typedef void* jobject;
typedef jobject jstring;
可见jstring就是⼀个void *类型的指针。
3.1 java->native
java虚拟机传递下来的字符串是存储在java虚拟机内部的字符串,这个字符串当然使⽤的是Unicode编码了,使⽤c编程的时候,传递下来的jstring类型是⼀个void *类型的指针,它指向java虚拟机内部的⼀个字符串,我们不能使⽤这个字符串,是因为它的编码⽅式是Unicode编码,我们需要把它转换为utf-8编码格式,这样我们就可以在
c/c++中访问这个转换后的字符串了。
我们可以使⽤jni规范提供的⼀下连个函数转换Unicode编码和utf-8编码:
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
void (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);
使⽤GetStringUTFChars函数时,要记得检测其返回值,因为调⽤该函数会有内存分配操作,失败后,该函数返回NULL,并抛OutOfMemoryError异常。
调⽤完GetStringUTFChars函数后,我们还要调⽤ReleaseStringUTFChars函数释放在GetStringUTFChars中分配的内存。不释放的话就会出现内存泄漏了。
3.2 native->java
有了以上两个函数,我们就可以把java中的字符串转换为c/c++中使⽤的字符串了,⽽把c/c++使⽤的字符串转换为java使⽤的字符串这件事我们之前已经做过了,我们可以使⽤使⽤NewStringUTF构造java.lang.String;如果此时没有⾜够的内存,NewStringUTF将抛OutOfMemoryError异常,同时返回NULL。
NewStringUTF定义如下:
jstring (*NewStringUTF)(JNIEnv*, const char*);
可见它接受⼀个char 类型的指针,char 就是我们可以在c/c++中⽤来指向字符串的指针了。
通过以上的学习,我们可以尝试使⽤这三个⽅法做个验证了,还是在原来的基础上修改.
3.3实站
这次实战,我们要开始使⽤android的logcat⼯具了,这个⼯具可以打印⼀些Log出来,⽅便我们使⽤。使⽤android logcat只需三步:
1.包含头⽂件
#include <android/log.h>
2.配置Android.mk
在Android.mk中添加: LOCAL_LDLIBS := -llog
3.定义LOGI,LOGE等宏
在使⽤前:
#define LOG_TAG "HELLO_JNI"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
这样我们就可以使⽤LOGI,LOGE来打印我们的Log了。
jni⽅法只是改为传递⼀个字符串进去:
jstring native_sayHello(JNIEnv * env, jobject obj,jstring str){
LOGE("JNI: native_sayHello");
char array[50];
char array1[50];
const char * local_str = (*env)->GetStringUTFChars(env,str,NULL);
LOGE("local_str: %s,length:%d",local_str,strlen(local_str));
strncpy(array,local_str,strlen(local_str)+1);
(*env)->ReleaseStringUTFChars(env,str,local_str);
LOGE("array: %s",array);
snprintf(array1,sizeof(array),"jni : %s",array);
LOGE("array1: %s",array1);
return (*env)->NewStringUTF(env,array1);
}
我们只是做了轻微的改动,native_sayHello接受⼀个jstring类型的参数,我们把这个参数转换为utf-8格式的字符串,然后添加⼀点我们Local的信息,然后再返回给java 层。假如我们在android中这样调⽤native⽅法:
String hehe = this.sayHello(“say hello to jni”);
此时,app就会显⽰:
jni:java say hello to jni
以上我们对java字符串和c/c++之间字符串的转换做了简单的学习和尝试。jni规范还提供了许多⽤于字
符串处理的函数:
jsize (*GetStringLength)(JNIEnv*, jstring);
const jchar* (*GetStringChars)(JNIEnv*, jstring, jboolean*);
void (*ReleaseStringChars)(JNIEnv*, jstring, const jchar*);
这三个函数⽤于操作unicode字符串。当jstring指向⼀个unicode字符串时,我们可以使⽤GetStringLength获取这个unicode字符串的长度。unicode字符串不同于utf-8格式的字符串,utf-8格式的字符串⼀‘\0’结尾,unicode编码的字符串则不同,因此,我们需要GetStringLength来获得unicode字符串的长度。
GetStringChars与ReleaseStringChars⽤来获取和释放unicode编码的字符串,⼀般不怎么⽤,但如果⽀持unicode编码的话,这两个函数会很有⽤。GetStringChars与GetStringUTFChars的第三个参数需要做进⼀步解释。如果我们使⽤他们中的某⼀个函数从JVM中获得⼀个字符串,我们不知道这个字符串是指向JVM 中的原始字符串还是是⼀份原始字符串的拷贝,但我们可以通过第三个参数来得知这⼀信息。这⼀信息是有⽤的,我们知道JVM中的字符串是不能更改的,如果获得的字符串是JVM中的原始字符串,第三个参数就为JNI_FALSE,那我们不可以修改它,但如果它是⼀份拷贝,第三个参数就为JNI_TRUE,则意味着我们可以修改它。
通常我们不关⼼它,只需要把第三个参数置为NULL即可。
四.jdk 1.2中新的字符串操作函数
4.1Get/RleaseStringCritical
为尽可能的避免内存分配,返回指向java.lang.String内容的指针,Java 2 SDKrelease 1.2提供了:Get/RleaseStringCritical. 这对函数有严格的使⽤原则。当使⽤这对函数时,这对函数间的代码应被当做临界区(critical region). 在该代码区,不要调⽤任何会阻塞当前线程和分配对象的JNI函数,如IO之类的操作。上述原则,可以避免JavaVM 执⾏GC。因为在执⾏Get/ReleaseStringCritical区的代码
时,GC被禁⽤了,如果因某些原因在其他线程中引发了JavaVM执⾏GC操作,VM有死锁的危险:当前线程A进⼊Get/RelaseStringCritical区,禁⽤了GC,如果其他线程B中有GC请求,因A线程禁⽤了GC,所以B线程被阻塞了;⽽此时,如果B线程被阻塞时已经获得了⼀个A线程执⾏后续⼯作时需要的锁;死锁发⽣了。
jni不⽀持Get/RleaseStringUTFCritical这样的函数,因为⼀旦涉及到字符串编码的转换,便使java虚拟机产⽣对数据的赋值⾏为,这样⽆法避免没存分配。
为避免死锁,此时应尽量避免调⽤其他JNI⽅法。
4.2GetStringRegion/GetStringUTFRegion
这两个函数的作⽤是向准备好的缓冲区写数据进去。
void (*GetStringRegion)(JNIEnv*, jstring, jsize, jsize, jchar*);
void (*GetStringUTFRegion)(JNIEnv*, jstring, jsize, jsize, char*);
简单⽤法举例:
char outbuf[128];
int len = (*env)->GetStringLength(env, prompt);
(*env)->GetStringUTFRegion(env, prompt, 0, len, outbuf);
其中prompt是java层传下来的字符串。这个函数可以直接把jstring类型的字符串写⼊到outbuf中。这个函数有三个参数:第⼀个是outbuf的其实位置,第⼆个是写⼊数据的长度,第三个参数是outbuf。注意,数据的长度是unicode编码的字符串长度,我们需要使⽤GetStringLength来获取。
注:GetStringLength/GetStringUTFLength这两个函数,前者是Unicode编码长度,后者
是UTF编码长度。
五.jni字符串操作函数总结
且看下图:
六.jni字符串操作函数总结
对于⼩尺⼨字串的操作,⾸选Get/SetStringRegion和Get/SetStringUTFRegion,因为栈
上空间分配,开销要⼩的多;⽽且没有内存分配,就不会有out-of-memory exception。如
果你要操作⼀个字串的⼦集,这两个函数函数的starting index和length正合要求。springboot查询数据返回页面
GetStringCritical/ReleaseStringCritical函数的使⽤必须⾮常⼩⼼,他们可能导致死锁。
七.访问数组
JNI处理基本类型数组和对象数组的⽅式是不同的。
7.1访问基本类型数组
JNI⽀持通过Get/ReleaseArrayElemetns返回Java数组的⼀个拷贝(实现优良的VM,会返回指向Java数组的⼀个直接的指针,并标记该内存区域,不允许被GC)。jni.h中的定义如下:
jboolean* (*GetBooleanArrayElements)(JNIEnv*, jbooleanArray, jboolean*);
jbyte* (*GetByteArrayElements)(JNIEnv*, jbyteArray, jboolean*);
jchar* (*GetCharArrayElements)(JNIEnv*, jcharArray, jboolean*);
jshort* (*GetShortArrayElements)(JNIEnv*, jshortArray, jboolean*);
jint* (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
jlong* (*GetLongArrayElements)(JNIEnv*, jlongArray, jboolean*);
jfloat* (*GetFloatArrayElements)(JNIEnv*, jfloatArray, jboolean*);
jdouble* (*GetDoubleArrayElements)(JNIEnv*, jdoubleArray, jboolean*);
void (*ReleaseBooleanArrayElements)(JNIEnv*, jbooleanArray,
jboolean*, jint);
void (*ReleaseByteArrayElements)(JNIEnv*, jbyteArray,
jbyte*, jint);
void (*ReleaseCharArrayElements)(JNIEnv*, jcharArray,
jchar*, jint);
void (*ReleaseShortArrayElements)(JNIEnv*, jshortArray,
jshort*, jint);
void (*ReleaseIntArrayElements)(JNIEnv*, jintArray,
jint*, jint);
void (*ReleaseLongArrayElements)(JNIEnv*, jlongArray,
jlong*, jint);
void (*ReleaseFloatArrayElements)(JNIEnv*, jfloatArray,
jfloat*, jint);
void (*ReleaseDoubleArrayElements)(JNIEnv*, jdoubleArray,
jdouble*, jint);
GetArrayRegion函数可以把获得的数组写⼊⼀个提前分配好的缓冲区中。SetArrayRegion可以操作这⼀缓冲区。
其定义如下:
void (*GetBooleanArrayRegion)(JNIEnv*, jbooleanArray,
jsize, jsize, jboolean*);
void (*GetByteArrayRegion)(JNIEnv*, jbyteArray,
jsize, jsize, jbyte*);
void (*GetCharArrayRegion)(JNIEnv*, jcharArray,
jsize, jsize, jchar*);
void (*GetShortArrayRegion)(JNIEnv*, jshortArray,
jsize, jsize, jshort*);
void (*GetIntArrayRegion)(JNIEnv*, jintArray,
jsize, jsize, jint*);
void (*GetLongArrayRegion)(JNIEnv*, jlongArray,
jsize, jsize, jlong*);
文件格式转换appvoid (*GetFloatArrayRegion)(JNIEnv*, jfloatArray,
jsize, jsize, jfloat*);
void (*GetDoubleArrayRegion)(JNIEnv*, jdoubleArray,
jsize, jsize, jdouble*);
/* spec shows these without const; some jni.h do, some don't */
matlab如何绘制数列图像int函数参数void (*SetBooleanArrayRegion)(JNIEnv*, jbooleanArray,
jsize, jsize, const jboolean*);
void (*SetByteArrayRegion)(JNIEnv*, jbyteArray,
jsize, jsize, const jbyte*);
void (*SetCharArrayRegion)(JNIEnv*, jcharArray,
jsize, jsize, const jchar*);
void (*SetShortArrayRegion)(JNIEnv*, jshortArray,
jsize, jsize, const jshort*);
void (*SetIntArrayRegion)(JNIEnv*, jintArray,
jsize, jsize, const jint*);
void (*SetLongArrayRegion)(JNIEnv*, jlongArray,
jsize, jsize, const jlong*);
void (*SetFloatArrayRegion)(JNIEnv*, jfloatArray,
jsize, jsize, const jfloat*);
void (*SetDoubleArrayRegion)(JNIEnv*, jdoubleArray,
jsize, jsize, const jdouble*);
GetArrayLength函数可以获得数组中元素个数,其定义如下:
jsize (*GetArrayLength)(JNIEnv*, jarray);
jni操作原始类型数组的函数总结如下:
7.2实战尝试
我们修改之前的native_sayHello函数,让它接受数组作为参数:
jint native_sayHello(JNIEnv * env, jobject obj,jintArray arr){
jint *carr;
jint i, sum = 0;
carr = (*env)->GetIntArrayElements(env, arr, NULL);
if (carr == NULL) {
return 0; /* exception occurred */
}
jint length = (*env)->GetArrayLength(env,arr);
for (i=0; i<length; i++) {
LOGE("carr[%d]=%d",i,carr[i]);
sum += carr[i];
}
(*env)->ReleaseIntArrayElements(env, arr, carr, 0);
return sum;
}
不要忘记修改函数签名:
static JNINativeMethod gMethods[] = {
{"sayHello", "([I)I", (void *)native_sayHello},
};
java层调⽤如下:
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
textView = (TextView) findViewById();
int a[] = {1,2,3,4,5,6,7,8,9};
String hehe = "num: "+String.valueOf(this.sayHello(a));
textView.setText(hehe);
}
public native int sayHello(int [] arr);
打印如下:
09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[0]=1
09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[1]=2
09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[2]=3
09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[3]=4
09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[4]=5
09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[5]=6
mysql管理工具哪个好09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[6]=7
09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[7]=8
09-22 12:17:54.365 28093-28093/com.jinwei.jnitesthello E/HELLO_JNI: carr[8]=9
7.3访问对象数组
对于对象数组的访问,使⽤Get/SetObjectArrayElement,对象数组只提供针对数组的每个元素的Get/Set,不提供类似Region的区域性操作。
对象数组的操作主要有如下三个函数:
jobjectArray (*NewObjectArray)(JNIEnv*, jsize, jclass, jobject);
jobject (*GetObjectArrayElement)(JNIEnv*, jobjectArray, jsize);
void (*SetObjectArrayElement)(JNIEnv*, jobjectArray, jsize, jobject);
这三个⽅法的使⽤通过下⾯的实战来讲解。
7.4实战体验
我们在下⾯的实战中作如下尝试:
1.java层传⼊⼀个String a[] = {“hello1”,”hello2”,”hello3”,”hello4”,”hello5”};
2.native打印出每⼀个值。
3.native创建⼀个String数组,
4.返回给java,java显⽰出这个字符串数组的所有成员
native实现⽅法:
我们新增⼀个函数来实现上⾯要求。
jobjectArray native_arrayTry(JNIEnv * env, jobject obj,jobjectArray arr){
jint length = (*env)->GetArrayLength(env,arr);
const char * tem ;
jstring larr;
jint i=0;
for(i=0;i<length;i++){
//1.获得数组中⼀个对象
larr = (*env)->GetObjectArrayElement(env,arr,i);
//2.转化为utf-8字符串
tem = (*env)->GetStringUTFChars(env,larr,NULL);
//3.打印这个字符串
LOGE("arr[%d]=%s",i,tem);
(*env)->ReleaseStringUTFChars(env,larr,tem);
}
jobjectArray result;
jint size = 5;
char buf[20];
//1.获取java.lang.String Class
jclass intArrCls = (*env)->FindClass(env,"java/lang/String");
if (intArrCls == NULL) {
return NULL; /* exception thrown */
}
//2. 创建java.lang.String数组
result = (*env)->NewObjectArray(env, size, intArrCls, NULL);
if (result == NULL) {
return NULL; /* out of memory error thrown */
}
//3.设置数组中的每⼀项,分别为jni0,jni1,jni2,jni3,jni4
for (i = 0; i < size; i++) {
larr = (*env)->GetObjectArrayElement(env,result,i);
snprintf(buf,sizeof(buf),"jni%d",i);
(*env)->SetObjectArrayElement(env, result, i,(*env)->NewStringUTF(env,buf) );
}
//4.返回array
return result;
}
⽅法签名:
static JNINativeMethod gMethods[] = {
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论