JNI程式設計指南(一):基本型別、字串、陣列
前言
對於任何一個初學者,學習JNI都是從Java和C/C++之間如何傳遞資料,以及資料型別之間是如何相互對映開始。
Native方法和C函式原型
看點程式碼
package com.net168.xxx class Simple { private native String testA(String str); private native static void testA(int num); } //C端原始碼 JNIEXPORT jstring JNICALL Java_Com_net168_xxx_Simple_testA(JNIEnv *env, jobject thiz, jstring str); JNIEXPORT void JNICALL Java_Com_net168_xxx_Simple_testB(JNIEnv *env, jclass clz, jint num);
知識點
-
C函式方法格式:
JNIEXPORT 返回型別 JNICALL Java_包名_方法名(JNIEnv *env, jobect/jclass thiz, 入參列表)
- 本地方法存在過載情況時,會有雙下劃線"__",後面跟著引數描述符,也就是長函式名;VM連線優先連結短函式名,然後連結長函式名,如果存在兩個過載的本地方法,則只會連結長函式名。
-
連結函式還可以通過JNI的
RegisterNatives
來註冊與一個類關聯的本地方法。 -
JNIEXPORT
和JNICALL
是定義在jni.h
裡面的兩個巨集,用來確保函式在本地庫外可見,C編譯時會進行正確轉換。 -
JNIEnv
是一個介面指標,指向若干個函式表,提供了JNI函式幫助C函式訪問JVM裡面的資料結構。 - 本地方法是靜態方法時,C函式的第二個變數是jclass,代表本地方法所在的類;如果是一個例項方法時,其變數的型別是jobject,代表本地方法所在的物件例項。
型別對映
本地方法宣告中的引數型別在C語言中都有對應的型別,具體對應表格如下:
java型別 | 本地型別 | 描述 |
---|---|---|
boolean | jboolean | C/C++8位整型 |
byte | jbyte | C/C++帶符號的8位整型 |
char | jchar | C/C++無符號的16位整型 |
short | jshort | C/C++帶符號的16位整型 |
int | jint | C/C++帶符號的32位整型 |
long | jlong | C/C++帶符號的64位整型e |
float | jfloat | C/C++32位浮點型 |
double | jdouble | C/C++64位浮點型 |
Object | jobject | 任何Java物件,或者沒有對應java型別的物件 |
Class | jclass | Class物件 |
String | jstring | 字串物件 |
Object[] | jobjectArray | 任何物件的陣列 |
boolean[] | jbooleanArray | 布林型陣列 |
byte[] | jbyteArray | 位元型陣列 |
char[] | jcharArray | 字元型陣列 |
short[] | jshortArray | 短整型陣列 |
int[] | jintArray | 整型陣列 |
long[] | jlongArray | 長整型陣列 |
float[] | jfloatArray | 浮點型陣列 |
double[] | jdoubleArray | 雙浮點型陣列 |
知識點
- Java裡面有兩種型別:基本型別和引用型別,JNI對這兩個型別的處理方式是不同的。
- JNI把Java中的物件當做一個C指標傳遞到本地方法中,這個指標指向JVM的內部資料結構,也就是其在記憶體中的儲存方式是不可見的,必須通過JNI函式來操作JVM中的物件。
字串處理
jstring轉c語言字串
JNIEXPORT jstring JNICALL Java_Com_net168_xxx_Simple_testA(JNIEnv *env, jobject thiz, jstring jstr) { jboolean isCopy; //獲取utf-8格式的c字串 const char *str1 = env->GetStringUTFChars(jstr, &isCopy); //do something env->ReleaseStringUTFChars(jstr, str1); //獲取Unicode格式的c字串 const jchar *str2 = env->GetStringChars(jstr, &isCopy); //do something env->ReleaseStringChars(jstr, str2); }
知識點
-
GetStringUTFChars()
可以將jstring轉換成UTF-8編碼格式的c字串,GetStringChars()
可以將jstring轉換成Unicode編碼格式的c字串。 -
獲取c字串需要判斷
if(str == NULL)
,原因可能是JVM需要為這個字串分配記憶體,會由於記憶體不足導致失敗,丟擲OutOfMemoryError
異常。 -
對於第二個引數
isCopy
,如果c字串是指向JVM中jstring的同一份資料時為JNI_FALSE
;如果c字串是jstring的一份記憶體拷貝則為JNI_TRUE
。若為JNI_FALSE
我們不可能修改該c字串,會破壞Java語言String不可變的原則。一般我們不需要關心是否複製的,那麼可以傳入NULL
。 -
一旦Java物件指標被傳遞給c程式碼,那麼GC就不會回收這個物件;所以我們需要呼叫
ReleaseStringUTFChars()
/ReleaseStringChars()
這兩個方法來釋放資源:如果是獲取了jstring的直接引用,則解除JVM的持有讓GC可以回收;如果是記憶體拷貝則回收釋放相應記憶體。 -
utf-8字串以
\0
結尾,而Unicode不是;所以當ReleaseStringUTFChars()
獲取一個編碼格式為Unicode的jstring時,返回的c字串並不一定以\0
結尾。建議直接以GetStringLength()
和GetStringUTFLength()
來獲取字串長度;對於strlen()
需要謹慎確保jstring指向的是一個utf-8的字串。
構造新字串
const char *str = "hello"; //將str轉為utf-8編碼的jstring字串 jstring jstr = env->NewStringUTF(str); const jchar *str1 = env->GetStringChars(jstr, NULL); //將str1轉為unicode編碼的jstring字串 jstring jstr1 = env->NewString(str1, env->GetStringLength(jstr));
知識點
-
獲取c字串需要判斷
if(jstr == NULL)
,如果JVM記憶體不足則會丟擲OutOfMemoryError異常,並返回NULL。 -
NewStringUTF()
不需要傳入字串長度,因為utf-8預設以/0
結尾;而NewString()
則需要在第二個引數傳入該字串的長度。
其他字串函式
//臨界區字串函式 const jchar *str = env->GetStringCritical(jstr, NULL); //do something env->ReleaseStringCritical(jstr, str); //預先分配快取字串函式 jchar *str1 = static_cast<jchar *>(malloc(5 * sizeof(jchar))); env->GetStringRegion(jstr, 0, 5, str1); //do something //自己釋放str1 malloc的記憶體 free(str1);
知識點
-
Get/ReleaseStringCritical
可以提交JVM返回直接指標的可能性,其會禁止GC的執行,但是其必須執行在"臨界區"中,也就是在這兩函式中間不能呼叫任何執行緒阻塞、或者本地JNI函式,否則容易引起死鎖。 -
Get/ReleaseStringRegion
和Get/ReleaseStringUTFRegion
對於小字串來說是最佳選擇,因為緩衝區可以提前分配;並且可以按需複製小段內容,因為它提供了一個開始索引和子字串長度。
陣列
基本型別資料陣列
JNIEXPORT void JNICALL Java_Com_net168_xxx_Simple_testC(JNIEnv *env, jobject thiz, jintArray jarray) { //獲取整個陣列內容 jint *array1 = env->GetIntArrayElements(jarray, NULL); //do something env->ReleaseIntArrayElements(jarray, array1, 0); //獲取陣列長度 jsize len = env->GetArrayLength(jarray); //預分配獲取陣列內容 jint buf[10]; env->GetIntArrayRegion(jarray, 0, 10, buf); //棧區域不用手動釋放記憶體 //在開始索引3的位置,開始更新5個數據 env->SetIntArrayRegion(jarray, 3, 5, buf); //臨界區獲取陣列內容 jint *array2 = static_cast<jint *>(env->GetPrimitiveArrayCritical(jarray, NULL)); //do something env->ReleasePrimitiveArrayCritical(jarray, array2, 0); }
知識點
Get/Release<Type>ArrayElements GetArrayLength Set/Get<Type>ArrayRegion Get/ReleasePrimitiveArrayCritical
物件陣列
JNIEXPORT void JNICALL Java_Com_net168_xxx_Simple_testD(JNIEnv *env, jobject thiz, jobjectArray jarray) { //獲取jobjectArray的第一個jobject jobject obj1 = env->GetObjectArrayElement(jarray, 0); //將obj1設定到陣列的第二個索引的位置 env->SetObjectArrayElement(jarray, 1, obj1); }
知識點
-
物件陣列不能一次性獲取整個陣列,需要用
GetObjectArrayElement
獲取指定索引位置的jobect物件,還有用SetObjectArrayElement
修改陣列指定位置的元素。
結語
後續會陸續釋出多篇JNI更加深入的文章。
本文同步釋出於簡書 、CSDN 。
End!