Android系統移植與平臺開發(九)- JNI介紹
JNI是在學習Android HAL時必須要面臨一個知識點,如果你不瞭解它的機制,不瞭解它的使用方式,你會被原生代碼繞的暈頭轉向,JNI作為一箇中間語言的翻譯官在執行Java程式碼的Android中有著重要的意義,這兒的內容比較多,也是最基本的,如果想徹底瞭解JNI的機制,請檢視:
本文結合了網友ljeagle寫的JNI學習筆記和自己通過JNI的手冊及Android中常用的部分寫得本文。
JNI學習筆記:
讓我們開始吧!!
----------------------------------------------------------------------------------------
JNI概念
JNI是本地語言程式設計介面。它允許執行在JVM中的Java程式碼和用C、C++或彙編寫的原生代碼相互操作。
在以下幾種情況下需要使用到JNI:
l 應用程式依賴於特定平臺,平臺獨立的Java類庫程式碼不能滿足需要
l 你已經有一個其它語言寫的一個庫,並且這個庫需要通過JNI來訪問Java程式碼
l 需要執行速度要求的程式碼實現功能,比如低階的彙編程式碼
通過JNI程式設計,你可以使用本地方法來:
l 建立、訪問、更新Java物件
l 呼叫Java方法
l 捕獲及丟擲異常
l 載入並獲得類資訊
l 執行執行時型別檢查
JNI的原理
JVM將JNI介面指標傳遞給本地方法,本地方法只能在當前執行緒中訪問該介面指標,不能將介面指標傳遞給其它執行緒使用。在VM中 JNI介面指標指向的區域用來分配和儲存執行緒本地資料。
載入和連結本地方法
在Java裡通過System.loadLibrary()來載入動態庫,但是,動態庫只能被載入一次,因此,通常動態庫的載入放在靜態初始化語句塊中。
package pkg; class Cls { native double f(int i, String s); // 宣告為本地方法 static { System.loadLibrary(“pkg_Cls”); // 通過靜態初始化語句塊來載入動態庫 } }
通常在動態庫中宣告大量的函式,這些函式被Java呼叫,這些本地函式由VM維護在一張函式指標陣列中,在本地方法裡通過呼叫JNI方法RegisterNatives()來註冊本地方法和Java方法的對映關係。
本地方法可以由C或C++來實現,C語言版本:
jdouble native_fun (
JNIEnv *env, /* interface pointer */
jobject obj, /* "this" pointer */
jint i, /* argument #1 */
jstring s) /* argument #2 */
{
/* Obtain a C-copy of the Java string */
const char *str = (*env)->GetStringUTFChars(env, s, 0);
/* process the string */
...
/* Now we are done with str */
(*env)->ReleaseStringUTFChars(env, s, str);
return ...
}
C++語言版本:
extern "C" /* specify the C calling convention */
jdouble native_fun (
JNIEnv *env, /* interface pointer */
jobject obj, /* "this" pointer */
jint i, /* argument #1 */
jstring s) /* argument #2 */
{
const char *str = env->GetStringUTFChars(s, 0);
...
env->ReleaseStringUTFChars(s, str);
return ...
}
由上面兩段程式碼對比可知,原生代碼使用C++來實現更簡潔。
兩段原生代碼第一個引數都是JNIEnv*env,它代表了VM裡的環境,原生代碼可以通過這個env指標對Java程式碼進行操作,例如:建立Java類物件,呼叫Java物件方法,獲取Java物件屬性等。jobject obj相當於Java中的Object型別,它代表呼叫這個本地方法的物件,例如:如果有new NativeTest.CallNative(),CallNative()是本地方法,本地方法第二個引數是jobject表示的是NativeTest類的物件的本地引用。
如果本地方法宣告為static型別
static jint native_get_count(JNIEnv* env, jobject thiz);
資料傳遞
l 基本型別
用Java程式碼呼叫C\C++程式碼時候,肯定會有引數資料的傳遞。兩者屬於不同的程式語言,在資料型別上有很多差別,應該要知道他們彼此之間的對應型別。例如,儘管C擁有int和long的資料型別,但是他們的實現卻是取決於具體的平臺。在一些平臺上,int型別是16位的,而在另外一些平臺上市32位的整數。基於這個原因,Java本地介面定義了jint,jlong等等。
Java Language Type | JNI Type |
boolean | jboolean |
byte | jbyte |
char | jchar |
short | jshort |
int | jint |
long | jlong |
float | jfloat |
double | jdouble |
All Reference type | jobject |
由Java型別和C/C++資料型別的對應關係,可以看到,這些新定義的型別名稱和Java型別名稱具有一致性,只是在前面加了個j,如int對應jint,long對應jlong。
我們看看jni.h和jni_md.h標頭檔案,可以更直觀的瞭解:
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble;
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;
由jni標頭檔案可以看出,jint對應的是C/C++中的long型別,即32位整數,而不是C/C++中的int型別(C/C++中的int型別長度依賴於平臺),它和Java 中int型別一樣。
所以如果要在本地方法中要定義一個jint型別的資料,規範的寫法應該是 jint i=123L;
再比如jchar代表的是Java型別的char型別,實際上在C/C++中卻是unsigned short型別,因為Java中的char型別為兩個位元組。而在C/C++中有這樣的定義:typedef unsigned short wchar_t。所以jchar就是相當於C/C++中的寬字元。所以如果要在本地方法中要定義一個jchar型別的資料,規範的寫法應該是jchar c=L'C';
實際上,所有帶j的型別,都是代表Java中的型別,並且jni中的型別介面與原生代碼在型別大小是完全匹配的,而在語言層次卻不一定相同。在本地方法中與JNI介面呼叫時,要在內部都要轉換,我們在使用的時候也需要小心。
l Java物件型別
Java物件在C\C++程式碼中的形式如下:
class _jclass : public _jobject {};
class _jthrowable : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jobjectArray : public _jarray {};
所有的_j開頭的類,都是繼承於_jobject,這也是Java語言的特別,所有的類都是Object的子類,這些類就是和Java中的類一一對應,只不過名字稍有不同而已。
1) jclass類和如何取得jclass物件
在Java中,Class型別代表一個Java類編譯的位元組碼,即:這個Java類,裡面包含了這個類的所有資訊。在JNI中,同樣定義了這樣一個類:jclass。瞭解反射的人都知道Class類是如何重要,可以通過反射獲得java類的資訊和訪問裡面的方法和成員變數。
JNIEnv有幾個方法可以取得jclass物件:
jclass FindClass(const char *name) {
return functions->FindClass(this, name);
}
FindClass會在系統classpath環境變數下尋找name類,注意包的間隔使用 “/ “,而不是”. “,如:
jclass cls_string=env->FindClass("java/lang/String");
獲得物件對應的jclass型別:
jclass GetObjectClass(jobject obj) {
return functions->GetObjectClass(this,obj);
}
獲得一個類的父類jclass型別:
jclass GetSuperclass(jclass sub) {
return functions->GetSuperclass(this,sub);
}
JNI本地方法訪問Java屬性和方法
在JNI呼叫中,不僅僅Java可以呼叫本地方法,原生代碼也可以呼叫Java中的方法和成員變數。在Java1.0中“原始的”Java到C的繫結中,程式設計師可以直接訪問物件資料域。然而,直接方法要求虛擬機器暴露他們的內部資料佈局,基於這個原因,JNI要求程式設計師通過特殊的JNI函式來獲取和設定資料以及呼叫java方法。
1) 取得代表屬性和方法的jfieldID和jmethodID
為了在C/C++中表示屬性和方法,JNI在jni.h標頭檔案中定義了jfieldID和jmethodID型別來分別代表Java物件的屬性和方法。我們在訪問或是設定Java屬性的時候,首先就要先在原生代碼取得代表該Java屬性的jfieldID,然後才能在原生代碼進行Java屬性操作。同樣的,我們需要呼叫Java物件方法時,也是需要取得代表該方法的jmethodID才能進行Java方法呼叫。
使用JNIEnv提供的JNI方法,我們就可以獲得屬性和方法相對應的jfieldID和jmethodID:
l GetFieldID :取得成員變數的id
l GetStaticFieldID :取得靜態成員變數的id
l GetMethodID :取得方法的id
l GetStaticMethodID :取得靜態方法的id
jfieldID GetFieldID(jclass clazz, const char *name,const char *sig)
jfieldID GetStaticFieldID(jclass clazz, const char*name, const char *sig)
jmethodID GetStaticMethodID(jclass clazz, const char*name, const char *sig)
jmethodID GetMethodID(jclass clazz, const char *name,constchar *sig)
可以看到這四個方法的引數列表都是一模一樣的,下面來分析下每個引數的含義:
第一個引數jclass clazz :
上一節講到的jclass型別,相當於Java中的Class類,代表一個Java類,而這裡面的代表的就是我們操作的Class類,我們要從這個類裡面取的屬性和方法的ID。
第二個引數constchar *name:
這是一個常量字元陣列,代表我們要取得的方法名或者變數名。
第三個引數constchar *sig:
這也是一個常量字元陣列,代表我們要取得的方法或變數的簽名。
什麼是方法或者變數的簽名呢?
我們來看下面的例子,如何來獲得屬性和方法ID:
public class NativeTest {
publicvoid show(int i){
System.out.println(i);
}
public void show(double d){
System.out.println(d);
}
}
原生代碼部分:
//首先取得要呼叫的方法所在的類的Class物件,在C/C++中即jclass物件
jclass clazz_NativeTest=env->FindClass("cn/itcast/NativeTest");
//取得jmethodID
jmethodID id_show=env->GetMethodID(clazz_NativeTest,“show”,"???");
上述程式碼中的id_show取得的jmethodID到底是哪個show方法呢?由於Java語言有方法過載的面向物件特性,所以只通過函式名不能明確的讓JNI找到Java裡對應的方法。所以這就是第三個引數sig的作用,它用於指定要取得的屬性或方法的型別簽名。
2) JNI簽名:
型別簽名 | Java 型別 | 型別簽名 | Java 型別 |
Z | boolean | [ | [] |
B | byte | [I | int[] |
C | char | [F | float[] |
S | short | [B | byte[] |
I | int | [C | char[] |
J | long | [S | short[] |
F | float | [D | double[] |
D | double | [J | long[] |
L | fully-qualified-class(全限定的類) | [Z | boolean[] |
l 基本型別
以特定的大寫字母表示
l 引用型別
Java物件以L開頭,然後以“/”分隔包的完整型別,例如String的簽名為:Ljava/lang/String;
在Java裡陣列型別也是引用型別,陣列以[ 開頭,後面跟陣列元素型別的簽名,例如:int[] 簽名就是[I ,對於二維陣列,如int[][] 簽名就是[[I,object陣列簽名就是[Ljava/lang/Object;
l 方法簽名
(引數1型別簽名引數2型別簽名引數3型別簽名.......)返回值型別簽名
注意:
函式名,在簽名中沒有體現出來
引數列表相挨著,中間沒有逗號,沒有空格
返回值出現在()後面
如果引數是引用型別,那麼引數應該為:L型別;
如果函式沒有返回值,也要加上V型別
例如:
Java方法 | 對應簽名 |
boolean isLedOn(void) ; | ()Z |
void setLedOn(int ledNo); | (I) |
String substr(String str, int idx, int count); | (Ljava/lang/String;II)Ljava/lang/String |
char fun (int n, String s, int[] value); | (ILjava/lang/String;[I)C |
boolean showMsg(View v, String msg); | (Landroid/View;Ljava/lang/String;)Z |
3) 根據獲取的ID,來取得和設定屬性,以及呼叫方法。
l 獲得、設定屬性和靜態屬性
取得了代表屬性和靜態屬性的jfieldID,就可以使用JNIEnv中提供的方法來獲取和設定屬性/靜態屬性。
獲取屬性/靜態屬性的形式:
Get<Type>Field GetStatic<Type>Field。
設定屬性/靜態屬性的形式:
Set<Type>Field SetStatic<Type>Field。
取得成員屬性:
jobject GetObjectField(jobjectobj, jfieldID fieldID);
jboolean GetBooleanField(jobjectobj, jfieldID fieldID);
jbyte GetByteField(jobjectobj, jfieldID fieldID);
取得靜態屬性:
jobject GetStaticObjectField(jclass clazz, jfieldID fieldID);
jboolean GetStaticBooleanField(jclass clazz, jfieldID fieldID);
jbyte GetStaticByteField(jclass clazz, jfieldID fieldID);
Get方法的第一個引數代表要獲取的屬性所屬物件或jclass物件,第二個引數即屬性ID。
設定成員屬性:
void SetObjectField(jobjectobj, jfieldID fieldID, jobject val);
void SetBooleanField(jobjectobj, jfieldID fieldID,jboolean val);
void SetByteField(jobjectobj, jfieldID fieldID, jbyte val);
設定靜態屬性:
void SetStaticObjectField(jobjectobj, jfieldID fieldID, jobject val);
void SetStaticBooleanField(jobjectobj, jfieldID fieldID,jboolean val);
void SetStaticByteField(jobjectobj, jfieldID fieldID, jbyte val);
Set方法的第一個引數代表要設定的屬性所屬的物件或jclass物件,第二個引數即屬性ID,第三個引數代表要設定的值。
l 呼叫方法
取得了代表方法和靜態方法的jmethodID,就可以用在JNIEnv中提供的方法來呼叫方法和靜態方法。
呼叫靜態方法:
CallStatic<Type>Method(jclass clazz, jmethodID methodID,...);
CallStatic<Type>MethodV(jclass clazz, jmethodID methodID,va_listargs);
CallStatic<Type>tMethodA(jclass clazz, jmethodID methodID,constjvalue *args);
上面的Type這個方法的返回值型別,如Int,Char,Byte等等。
第一個引數代表呼叫的這個方法所屬於的物件,或者這個靜態方法所屬的類。
第二個引數代表jmethodID。
後面的引數,就代表這個方法的引數列表了。
上述方法的呼叫有三種形式:
a) Call<Type>Method(jobject obj, jmethodIDmethodID,...);
// Java方法
public int show(int i,double d,char c){
…
}
// 本地呼叫Java方法
jint i=10L;
jdouble d=2.4;
jchar c=L'd';
env->CallIntMethod(obj, id_show, i, d, c);
b) Call<Type>MethodV(jobject obj, jmethodIDmethodID,va_list args)
這種方式使用較少。
c) Call<Type>MethodA(jobject obj, jmethodIDmethodID,jvalue* v)
這種呼叫方式其第三個引數是一個jvalue的指標。jvalue型別是在 jni.h標頭檔案中定義的聯合體union,看它的定義:
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;
例如:
jvalue * args=new jvalue[3];
args[0].i=12L;
args[1].d=1.2;
args[2].c=L'c';
jmethodID id_goo=env->GetMethodID(env->GetObjectClass(obj),"goo","(IDC)V");
env->CallVoidMethodA(obj,id_goo,args);
delete []args; //釋放記憶體
靜態方法的呼叫方式和成員方法呼叫一樣。
3.5 本地建立Java物件
1) 原生代碼建立Java物件
JNIEnv提供了下面幾個方法來建立一個Java物件:
jobject NewObject(jclass clazz, jmethodID methodID,...);
jobject NewObjectV(jclass clazz, jmethodID methodID,va_list args);
jobject NewObjectA(jclass clazz, jmethodID methodID,const jvalue *args) ;
本地建立Java物件的函式和前面本地呼叫Java方法很類似:
第一個引數jclass class 代表的你要建立哪個類的物件
第二個引數jmethodID methodID 代表你要使用哪個構造方法ID來建立這個物件。
只要有jclass和jmethodID ,我們就可以在本地方法建立這個Java類的物件。
指的一提的是:由於Java的構造方法的特點,方法名與類名一樣,並且沒有返回值,所以對於獲得構造方法的ID的方法env->GetMethodID(clazz,method_name ,sig)中的第二個引數是固定為類名,第三個引數和要呼叫的構造方法有關,預設的Java構造方法沒有返回值,沒有引數。例如:
jclass clazz=env->FindClass("java/util/Date"); //取得java.util.Date類的jclass物件
jmethodID id_date=env->GetMethodID(clazz,"Date","()V"); //取得某一個構造方法的jmethodID
jobject date=env->NewObject(clazz,id_date); //呼叫NewObject方法建立java.util.Date物件
2) 本地方法對Java字串的操作
在Java中,字串String物件是Unicoode(UTF-16)編碼,每個字元不論是中文還是英文還是符號,一個字元總是佔用兩個位元組。在C/C++中一個字元是一個位元組, C/C++中的寬字元是兩個位元組的。所以Java通過JNI介面可以將Java的字串轉換到C/C++的寬字串(wchar_t*),或是傳回一個UTF-8的字串(char*)到C/C++,反過來,C/C++可以通過一個寬字串,或是一個UTF-8編碼的字串建立一個Java端的String物件。
可以看下面的一個例子:
在Java端有一個字串 String str="abcde";,在本地方法中取得它並且輸出:
void native_string_operation (JNIEnv * env, jobject obj)
{
//取得該字串的jfieldID
jfieldID id_string=env->GetFieldID(env->GetObjectClass(obj), "str", "Ljava/lang/String;");
jstring string=(jstring)(env->GetObjectField(obj, id_string)); //取得該字串,強轉為jstring型別。
printf("%s",string);
}
由上面的程式碼可知,從java端取得的String屬性或者是方法返回值的String物件,對應在JNI中都是jstring型別,它並不是C/C++中的字串。所以,我們需要對取得的 jstring型別的字串進行一系列的轉換,才能使用。
JNIEnv提供了一系列的方法來操作字串:
l const jchar *GetStringChars(jstring str, jboolean*isCopy)
將一個jstring物件,轉換為(UTF-16)編碼的寬字串(jchar*)。
l const char *GetStringUTFChars(jstring str,jboolean *isCopy)
將一個jstring物件,轉換為(UTF-8)編碼的字串(char*)。
這兩個函式的引數中,第一個引數傳入一個指向Java 中String物件的jstring引用。第二個引數傳入的是一個jboolean的指標,其值可以為NULL、JNI_TRUE、JNI_FLASE。
如果為JNI_TRUE則表示開闢記憶體,然後把Java中的String拷貝到這個記憶體中,然後返回指向這個記憶體地址的指標。
如果為JNI_FALSE,則直接返回指向Java中String的記憶體指標。這時不要改變這個記憶體中的內容,這將破壞String在Java中始終是常量的規則。
如果是NULL,則表示不關心是否拷貝字串。
使用這兩個函式取得的字元,在不適用的時候,要分別對應的使用下面兩個函式來釋放記憶體。
RealeaseStringChars(jstring jstr, const jchar*str)
RealeaseStringUTFChars(jstring jstr, constchar* str)
第一個引數指定一個jstring變數,即要釋放的本地字串的資源
第二個引數就是要釋放的本地字串
3) 建立Java String物件
jstring NewString(const jchar *unicode, jsizelen) // 根據傳入的寬字串建立一個Java String物件
jstring NewStringUTF(const char *utf) // 根據傳入的UTF-8字串建立一個Java String物件
4) 返回Java String物件的字串長度
jsize GetStringLength(jstring jstr) //返回一個java String物件的字串長度
jsize GetStringUTFLength(jstring jstr) //返回一個java String物件經過UTF-8編碼後的字串長度
3.6 Java陣列在原生代碼中的處理
我們可以使用GetFieldID獲取一個Java陣列變數的ID,然後用GetObjectFiled取得該陣列變數到本地方法,返回值為jobject,然後我們可以強制轉換為j<Type>Array型別。
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;
j<Type>Array型別是JNI定義的一個物件型別,它並不是C/C++的陣列,如int[]陣列,double[]陣列等等。所以我們要把j<Type>Array型別轉換為C/C++中的陣列來操作。
JNIEnv定義了一系列的方法來把一個j<Type>Array型別轉換為C/C++陣列或把C/C++陣列轉換為j<Type>Array。
jsize GetArrayLength(jarray array) // 獲得陣列的長度
jobjectArray NewObjectArray(jsize len, jclass clazz, jobjectinit) // 建立物件陣列,指定其大小
jobject GetObjectArrayElement(jobjectArray array, jsizeindex) // 獲得陣列的指定元素
void SetObjectArrayElement(jobjectArray array, jsizeindex,jobject val) // 設定陣列元素
jbooleanArray NewBooleanArray(jsize len) // 建立Boolean陣列,指定其大小
jbyteArray NewByteArray(jsize len) //下面的都類似,建立對應型別的陣列,並指定大小
jcharArray NewCharArray(jsize len)
jshortArray NewShortArray(jsize len)
jintArray NewIntArray(jsize len)
jlongArray NewLongArray(jsize len)
jfloatArray NewFloatArray(jsize len)
jdoubleArray NewDoubleArray(jsize len)
// 獲得指定型別陣列的元素
jboolean * GetBooleanArrayElements(jbooleanArray array,jboolean *isCopy)
jbyte * GetByteArrayElements(jbyteArray array, jboolean*isCopy)
jchar * GetCharArrayElements(jcharArray array, jboolean*isCopy)
jshort * GetShortArrayElements(jshortArray array, jboolean*isCopy)
jint * GetIntArrayElements(jintArray array, jboolean*isCopy)
jlong * GetLongArrayElements(jlongArray array, jboolean*isCopy)
jfloat * GetFloatArrayElements(jfloatArray array,jboolean *isCopy)
jdouble * GetDoubleArrayElements(jdoubleArray array,jboolean *isCopy)
// 釋放指定陣列
void ReleaseBooleanArrayElements(jbooleanArray array,jboolean *elems,jint mode)
void ReleaseByteArrayElements(jbyteArray array,jbyte*elems,jint mode)
void ReleaseCharArrayElements(jcharArray array,jchar*elems,jint mode)
void ReleaseShortArrayElements(jshortArray array,jshort*elems,jint mode)
void ReleaseIntArrayElements(jintArray array,jint*elems,jint mode)
void ReleaseLongArrayElements(jlongArray array,jlong*elems,jint mode)
void ReleaseFloatArrayElements(jfloatArray array,jfloat*elems,jint mode)
void ReleaseDoubleArrayElements(jdoubleArray array,jdouble *elems,jint mode)
void * GetPrimitiveArrayCritical(jarray array, jboolean*isCopy)
void ReleasePrimitiveArrayCritical(jarray array, void*carray, jint mode)
void GetBooleanArrayRegion(jbooleanArray array,jsizestart, jsize len, jboolean *buf)
void GetByteArrayRegion(jbyteArray array,jsize start,jsize len, jbyte *buf)
void GetCharArrayRegion(jcharArray array,jsize start,jsize len, jchar *buf)
void GetShortArrayRegion(jshortArray array,jsize start,jsize len, jshort *buf)
void GetIntArrayRegion(jintArray array,jsize start,jsize len, jint *buf)
void GetLongArrayRegion(jlongArray array,jsize start,jsize len, jlong *buf)
void GetFloatArrayRegion(jfloatArray array,jsize start,jsize len, jfloat *buf)
void GetDoubleArrayRegion(jdoubleArray array,jsizestart, jsize len, jdouble *buf)
void SetBooleanArrayRegion(jbooleanArray array, jsizestart, jsize len,const jboolean *buf)
void SetByteArrayRegion(jbyteArray array, jsize start,jsize len,const jbyte *buf)
void SetCharArrayRegion(jcharArray array, jsize start,jsize len,const jchar *buf)
void SetShortArrayRegion(jshortArray array, jsizestart, jsize len,const jshort *buf)
void SetIntArrayRegion(jintArray array, jsize start,jsize len,const jint *buf)
void SetLongArrayRegion(jlongArray array, jsize start,jsize len,const jlong *buf)
void SetFloatArrayRegion(jfloatArray array, jsizestart, jsize len,const jfloat *buf)
void SetDoubleArrayRegion(jdoubleArray array, jsizestart, jsize len,const jdouble *buf)
上面是JNIEnv提供給原生代碼呼叫的陣列操作函式,大致可以分為下面幾類:
1) 獲取陣列的長度
jsize GetArrayLength(jarray array);
2) 物件型別陣列的操作
jobjectArray NewObjectArray(jsize len, jclass clazz,jobject init) // 建立
jobject GetObjectArrayElement(jobjectArray array, jsizeindex) // 獲得元素
void SetObjectArrayElement(jobjectArray array, jsizeindex,jobject val) // 設定元素
JNI沒有提供直接把Java的物件型別陣列(Object[ ])直接轉到C++中的jobject[ ]陣列的函式。而是直接通過Get/SetObjectArrayElement這樣的函式來對Java的Object[ ]陣列進行操作
3) 對基本資料型別陣列的操作
基本資料型別陣列的操作方法比較多,大致可以分為如下幾類:
Get<Type>ArrayElements/Realease<Type>ArrayElements;
Get<Type>ArrayElements(<Type>Array arr, jboolean*isCopied);
這類函式可以把Java基本型別的陣列轉換到C/C++中的陣列。有兩種處理方式,一是拷貝一份傳回原生代碼,另一種是把指向Java陣列的指標直接傳回到原生代碼,處理完本地化的陣列後,通過Realease<Type>ArrayElements來釋放陣列。處理方式由Get方法的第二個引數isCopied來決定。
Realease<Type>ArrayElements(<Type>Arrayarr,<Type>* array, jint mode)用這個函式可以選擇將如何處理Java和C/C++本地陣列:
其第三個引數mode可以取下面的值:
l 0:對Java的陣列進行更新並釋放C/C++的陣列
l JNI_COMMIT:對Java的陣列進行更新但是不釋放C/C++的陣列
l JNI_ABORT:對Java的陣列不進行更新,釋放C/C++的陣列
例如:
Test.java
public class Test {
privateint [] arrays=new int[]{1,2,3,4,5};
publicnative void show();
static{
System.loadLibrary("NativeTest");
}
publicstatic void main(String[] args) {
newTest().show();
}
}
本地方法:
void native_test_show(JNIEnv * env, jobject obj)
{
jfieldIDid_arrsys=env->GetFieldID(env->GetObjectClass(obj),"arrays","[I");
jintArrayarr=(jintArray)(env->GetObjectField(obj, id_arrsys));
jint*int_arr=env->GetIntArrayElements(arr,NULL);
jsizelen=env->GetArrayLength(arr);
for(inti=0; i<len; i++)
{
cout<<int_arr[i]<<endl;
}
env->ReleaseIntArrayElements(arr,int_arr,JNI_ABORT);
}
4) 物件型別的陣列Object[]:
JNI沒有提供直接把java的物件型別陣列(Object[])直接轉到c/c++中的jobject[]陣列的函式;而是直接通過GetObjectArrayElement (JNIEnv *env, jobjectArrayarray, jsize index)/ SetObjectArrayElement (JNIEnv *env, jobjectArrayarray, jsize index, jobject val)這樣的函式來對java的Object[]陣列進行操作。
注意:使用上述的函式也不用釋放任何資源。
1.7區域性引用與全域性引用
1) JNI中的引用變數
Java程式碼與原生代碼裡在進行引數傳遞與返回值複製的時候,要注意資料型別的匹配。對於int, char等基本型別直接進行拷貝即可,對於Java中的物件型別,通過傳遞引用實現。VM保證所有的Java物件正確的傳遞給了原生代碼,並且維持這些引用,因此這些物件不會被Java的gc(垃圾收集器)回收。因此,原生代碼必須有一種方式來通知VM原生代碼不再使用這些Java物件,讓gc來回收這些物件。
JNI將傳遞給原生代碼的物件分為兩種:區域性引用和全域性引用。
l 區域性引用:只在上層Java呼叫原生代碼的函式內有效,當本地方法返回時,區域性引用自動回收。
l 全域性引用:只有顯示通知VM時,全域性引用才會被回收,否則一直有效,Java的gc不會釋放該引用的物件。
預設的話,傳遞給原生代碼的引用是區域性引用。所有的JNI函式的返回值都是區域性引用。
jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
static jclass stringClass = NULL; //static 不能儲存一個區域性引用
jmethodID cid;
jcharArray elemArr;
jstring result;
if(stringClass == NULL) {
stringClass = (*env)->FindClass(env, "java/lang/String"); // 區域性引用
if(stringClass == NULL) {
return NULL; /* exception thrown */
}
}
/* It iswrong to use the cached stringClass here,
because itmay be invalid. */
cid =(*env)->GetMethodID(env, stringClass, "<init>","([C)V");
...
elemArr =(*env)->NewCharArray(env, len);
...
result =(*env)->NewObject(env, stringClass, cid, elemArr);
(*env)->DeleteLocalRef(env, elemArr);
return result;
}
2) 手動釋放區域性引用情況
雖然區域性引用會在原生代碼執行之後自動釋放,但是有下列情況時,要手動釋放:
l 原生代碼訪問一個很大的Java物件時,在使用完該物件後,原生代碼要去執行比較複雜耗時的運算時,由於原生代碼還沒有返回,Java收集器無法釋放該本地引用的物件,這時,應該手動釋放掉該引用物件。
/* A native method implementation */
JNIEXPORT void JNICALL
func(JNIEnv *env, jobject this)
{
lref =... /* a large Java object*/
... /* last use of lref */
(*env)->DeleteLocalRef(env, lref);
lengthyComputation(); /* maytake some time */
return; /* all local refs are freed */
}
這個情形的實質,就是允許程式在native方法執行期間,java的垃圾回收機制有機會回收native程式碼不在訪問的物件。
l 原生代碼建立了大量區域性引用,這可能會導致JNI區域性引用表溢位,此時有必要及時地刪除那些不再被使用的區域性引用。比如:在原生代碼裡建立一個很大的物件陣列。
for (i = 0; i < len; i++) {
jstring jstr= (*env)->GetObjectArrayElement(env, arr, i);
... /*process jstr */
(*env)->DeleteLocalRef(env, jstr);
}
在上述迴圈中,每次都有可能建立一個巨大的字串陣列。在每個迭代之後,native程式碼需要顯示地釋放指向字串元素的區域性引用。
l 建立的工具函式,它會被未知的程式碼呼叫,在工具函式裡使用完的引用要及時釋放。
l 不返回的本地函式。例如,一個可能進入無限事件分發的迴圈中的方法。此時在迴圈中釋放區域性引用,是至關重要的,這樣才能不會無限期地累積,進而導致記憶體洩露。
區域性引用只在建立它們的執行緒裡有效,原生代碼不能將區域性引用在多執行緒間傳遞。一個執行緒想要呼叫另一個執行緒建立的區域性引用是不被允許的。將一個區域性引用儲存到全域性變數中,然後在其它執行緒中使用它,這是一種錯誤的程式設計。
3) 全域性引用
在一個本地方法被多次呼叫時,可以使用一個全域性引用跨越它們。一個全域性引用可以跨越多個執行緒,並且在被程式設計師手動釋放之前,一直有效。和區域性引用一樣,全域性引用保證了所引用的物件不會被垃圾回收。
JNI允許程式設計師通過區域性引用來建立全域性引用, 全域性引用只能由NewGlobalRef函式建立。下面是一個使用全域性引用例子:
jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
static jclass stringClass = NULL;
...
if(stringClass == NULL) {
jclasslocalRefCls =
(*env)->FindClass(env, "java/lang/String");
if(localRefCls == NULL) {
return NULL;
}
/* Createa global reference */
stringClass = (*env)->NewGlobalRef(env, localRefCls);
/* Thelocal reference is no longer useful */
(*env)->DeleteLocalRef(env, localRefCls);
/* Is theglobal reference created successfully? */
if(stringClass == NULL) {
return NULL; /* out of memory exception thrown */
}
}
...
}
4) 釋放全域性引用
在native程式碼不再需要訪問一個全域性引用的時候,應該呼叫DeleteGlobalRef來釋放它。如果呼叫這個函式失敗,Java VM將不會回收對應的物件。
1.8 本地C程式碼中建立Java物件及本地JNI物件的儲存
1) Android中Bitmap物件的建立
通常在JVM裡建立Java的物件就是建立Java類的例項,再呼叫Java類的構造方法。而有時Java的物件需要在原生代碼裡建立。以Android中的Bitmap的構建為例,Bitmap中並沒有Java物件建立的程式碼及外部能訪問的構造方法,所以它的例項化是在JNI的c中實現的。
BitmapFactory.java中提供了得到Bitmap的方法,時序簡化為:
BitmapFactory.java->BitmapFactory.cpp -> GraphicsJNI::createBitmap() [graphics.cpp]
GraphicsJNI::createBitmap()[graphics.cpp]的實現:
jobjectGraphicsJNI::createBitmap(JNIEnv* env, SkBitmap*bitmap, bool isMutable,
jbyteArrayninepatch, intdensity)
{
SkASSERT(bitmap != NULL);
SkASSERT(NULL!= bitmap->pixelRef());
jobject obj=env->AllocObject(gBitmap_class);
if (obj) {
env->CallVoidMethod(obj,gBitmap_constructorMethodID,
(jint)bitmap,isMutable, ninepatch, density);
if(hasException(env)) {
obj =NULL;
}
}
return obj;
}
而gBitmap_class的得到是通過:
jclass c=env->FindClass("android/graphics/Bitmap");
gBitmap_class =(jclass)env->NewGlobalRef(c);
//gBitmap_constructorMethodID是Bitmap的構造方法(方法名用”<init>”)的jmethodID:
gBitmap_constructorMethodID=env->GetMethodID(gBitmap_class, "<init>", "(IZ[BI)V");
總結一下,c中如何訪問Java物件的屬性:
1) 通過JNIEnv::FindClass()找到對應的jclass;
2) 通過JNIEnv::GetMethodID()找到類的構造方法的jfieldID;
3) 通過JNIEnv::AllocObject建立該類的物件;
4) 通過JNIEnv::CallVoidMethod()呼叫Java物件的構造方法。
2) 本地JNI物件儲存在Java環境中
C程式碼中某次被呼叫時生成的物件,在其他函式呼叫時是不可見的,雖然可以設定全域性變數但那不是好的解決方式,Android中通常是在Java域中定義一個int型的變數,在原生代碼生成物件的地方,與這個Java域的變數關聯,在別的使用到的地方,再從這個變數中取值。
以JNICameraContext為例來說明:
JNICameraContext是android_hardware_camera.cpp中定義的型別,並會在原生代碼中生成物件並與Java中android.hardware.Camera的mNativeContext關聯。
在註冊native函式之前,C中就已經把Java域中的屬性的jfieldID得到了。通過下列方法:
jclass clazz =env->FindClass("android/hardware/Camera ");
jfieldID field = env->GetFieldID(clazz, "mNativeContext","I");
如果執行成功,把field儲存到fileds.context成員變數中。
生成cpp物件時,通過JNIEnv::SetIntField()設定為Java物件的屬性
static void android_hardware_Camera_native_setup(JNIEnv*env, jobject thiz,
jobjectweak_this, jintcameraId)
{
// …
sp<JNICameraContext>context = new JNICameraContext(env, weak_this,clazz, camera);
// …
// 該處通過context.get()得到context物件的地址,儲存到了Java中的mNativeContext屬性裡
env->SetIntField(thiz,fields.context, (int)context.get());
}
而要使用時,又通過JNIEnv::GetIntField()獲取Java物件的屬性,並轉化為JNICameraContext型別:
JNICameraContext* context=reinterpret_cast<JNICameraContext*>(env->GetIntField(thiz,fields.context));
if (context!= NULL) {
// …
}
總結一下,c++中生成的物件如何儲存和使用:
1) 通過JNIEnv::FindClass()找到對應的jclass;
2) 通過JNIEnv::GetFieldID()找到類中屬性的jfieldID;
3) 某個呼叫過程中,生成cpp物件時,通過JNIEnv::SetIntField()設定為Java物件的屬性;
4) 另外的呼叫過程中,通過JNIEnv::GetIntField()獲取Java物件的屬性,再轉化為真實的物件型別。