Android jni/ndk編程二:jni數據類型轉換(primitive,String,array)

一.數據類型映射概述

從咱們開始jni編程起,就不可能避開函數的參數與返回值的問題。java語言的數據類型和c/c++有不少不一樣的地方,因此咱們必須考慮當在java層調用c/c++函數時,怎麼正確的把java的參數傳給c/c++函數,怎麼正確的從c/c++函數獲取正確的函數返回值;反之,當咱們在c/c++中使用java的方法或屬性時,如何確保數據類型能正確的在java和c/c++之間轉換。 
回顧咱們上一篇文章中的那個c函數:java

#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,第三個參數起纔是該函數要接受的參數,因此這裏說它沒有接受參數。android

1.1JNIEnv * env

JNIEnv是一個線程相關的結構體, 該結構體表明瞭 Java 在本線程的運行環境 。這意味不一樣的線程各自擁有各一個JNIEnv結構體,且彼此之間互相獨立,互不干擾。NIEnv結構包括了JNI函數表,這個函數表中存放了大量的函數指針,每個函數指針又指向了具體的函數實現,好比,例子中的NewStringUTF函數就是這個函數表中的一員。 
JVM,JNIEnv與native function的關係可用以下圖來表述: 
這裏寫圖片描述c++

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 數據類型。ruby

2.1實踐

咱們在上一篇博客中實現的程序的基礎上進一步實驗,如今給native_sayHello函數添加一個參數,c代碼以下:app

#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;」 
Android層的代碼也隨之改變:ide

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.text); 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 {}; 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(「Java 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正合要求。

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*); void (*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 */ 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) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.text); 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 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[] = { {"sayHello", "([I)I", (void *)native_sayHello}, {"arrayTry","([Ljava/lang/String;)[Ljava/lang/String;",(void *)native_arrayTry}, }; 

java層調用:

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.text); String a[] = {"hello1","hello2","hello3","hello4","hello5"}; String []strings = this.arrayTry(a); StringBuilder stringBuilder = new StringBuilder(); for(String s:strings){ stringBuilder.append(s); } textView.setText(stringBuilder.toString()); } public native int sayHello(int []arr); public native String[] arrayTry(String [] arr); }
相關文章
相關標籤/搜索