從咱們開始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
JNIEnv是一個線程相關的結構體, 該結構體表明瞭 Java 在本線程的運行環境 。這意味不一樣的線程各自擁有各一個JNIEnv結構體,且彼此之間互相獨立,互不干擾。NIEnv結構包括了JNI函數表,這個函數表中存放了大量的函數指針,每個函數指針又指向了具體的函數實現,好比,例子中的NewStringUTF函數就是這個函數表中的一員。
JVM,JNIEnv與native function的關係可用以下圖來表述: c++
這個參數的意義取決於該方法是靜態仍是實例方法(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
咱們在上一篇博客中實現的程序的基礎上進一步實驗,如今給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 *類型的指針。
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中分配的內存。不釋放的話就會出現內存泄漏了。
有了以上兩個函數,咱們就能夠把java中的字符串轉換爲c/c++中使用的字符串了,而把c/c++使用的字符串轉換爲java使用的字符串這件事咱們以前已經作過了,咱們可使用使用NewStringUTF構造java.lang.String;若是此時沒有足夠的內存,NewStringUTF將拋OutOfMemoryError異常,同時返回NULL。
NewStringUTF定義以下:
jstring (*NewStringUTF)(JNIEnv*, const char*);
可見它接受一個char 類型的指針,char 就是咱們能夠在c/c++中用來指向字符串的指針了。
經過以上的學習,咱們能夠嘗試使用這三個方法作個驗證了,仍是在原來的基礎上修改.
此次實戰,咱們要開始使用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便可。
爲儘量的避免內存分配,返回指向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方法。
這兩個函數的做用是向準備好的緩衝區寫數據進去。
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編碼長度。
且看下圖:
對於小尺寸字串的操做,首選Get/SetStringRegion和Get/SetStringUTFRegion,由於棧
上空間分配,開銷要小的多;並且沒有內存分配,就不會有out-of-memory exception。如
果你要操做一個字串的子集,這兩個函數函數的starting index和length正合要求。
GetStringCritical/ReleaseStringCritical函數的使用必須很是當心,他們可能致使死鎖。
JNI處理基本類型數組和對象數組的方式是不一樣的。
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操做原始類型數組的函數總結以下:
咱們修改以前的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
對於對象數組的訪問,使用Get/SetObjectArrayElement,對象數組只提供針對數組的每
個元素的Get/Set,不提供相似Region的區域性操做。
對象數組的操做主要有以下三個函數:
jobjectArray (*NewObjectArray)(JNIEnv*, jsize, jclass, jobject);
jobject (*GetObjectArrayElement)(JNIEnv*, jobjectArray, jsize); void (*SetObjectArrayElement)(JNIEnv*, jobjectArray, jsize, jobject);
這三個方法的使用經過下面的實戰來說解。
咱們在下面的實戰中做以下嘗試:
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); }