一.數據類型映射概述
從我們開始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;”
Android層的代碼也隨之改變:
@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); }