編輯:Android開發實例
JNI是在學習Android HAL時必須要面臨一個知識點,如果你不了解它的機制,不了解它的使用方式,你會被本地代碼繞的暈頭轉向,JNI作為一個中間語言的翻譯官在運行Java代碼的Android中有著重要的意義,這兒的內容比較多,也是最基本的,如果想徹底了解JNI的機制,請查看:
http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/design.html
本文結合了網友ljeagle寫的JNI學習筆記和自己通過JNI的手冊及Android中常用的部分寫得本文。
JNI學習筆記:
http://www.fengfly.com/plus/view-209787-1.html
讓我們開始吧!!
----------------------------------------------------------------------------------------
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代碼調用本地方法時,VM將JNI接口指針作為參數傳遞給本地方法,當同一個Java線程調用本地方法時VM保證傳遞給本地方法的參數是相同的。不過,不同的Java線程調用本地方法時,本地方法接收到的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類型:
- jclass GetObjectClass(jobject obj) {
- return functions->GetObjectClass(this,obj);
- }
jclass GetSuperclass(jclass sub) {
return functions->GetSuperclass(this,sub);
}
在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)
可以看到這四個方法的參數列表都是一模一樣的,下面來分析下每個參數的含義:
第一個參數jclassclazz :
上一節講到的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(jclassclazz, jfieldID fieldID);
jboolean GetStaticBooleanField(jclassclazz, jfieldID fieldID);
jbyte GetStaticByteField(jclassclazz, 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中提供的方法來調用方法和靜態方法。
調用普通方法:
Call<Type>Method(jobject obj, jmethodID methodID,...);
Call<Type>MethodV(jobject obj, jmethodID methodID,va_listargs);
Call<Type>tMethodA(jobject obj, jmethodID methodID,constjvalue *args);
調用靜態方法:
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';
jmethodIDid_goo=env->GetMethodID(env->GetObjectClass(obj),"goo","(IDC)V");
env->CallVoidMethodA(obj,id_goo,args);
delete []args; //釋放內存
靜態方法的調用方式和成員方法調用一樣。
1) 本地代碼創建Java對象
JNIEnv提供了下面幾個方法來創建一個Java對象:
jobject NewObject(jclass clazz, jmethodID methodID,...);
jobject NewObjectV(jclass clazz, jmethodIDmethodID,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構造方法沒有返回值,沒有參數。例如:
jclassclazz=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
jfieldIDid_string=env->GetFieldID(env->GetObjectClass(obj), "str", "Ljava/lang/String;");
jstringstring=(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編碼後的字符串長度
我們可以使用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) // 設置數組元素
jbooleanArrayNewBooleanArray(jsize len) // 創建Boolean數組,指定其大小
jbyteArrayNewByteArray(jsize len) //下面的都類似,創建對應類型的數組,並指定大小
jcharArrayNewCharArray(jsize len)
jshortArrayNewShortArray(jsize len)
jintArrayNewIntArray(jsize len)
jlongArrayNewLongArray(jsize len)
jfloatArrayNewFloatArray(jsize len)
jdoubleArrayNewDoubleArray(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(jbooleanArrayarray,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(jdoubleArrayarray,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, jclassclazz,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);
}
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 jclassstringClass = NULL; //static 不能保存一個局部引用
jmethodID cid;
jcharArrayelemArr;
jstringresult;
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);
returnresult;
}
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 jclassstringClass = 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) 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對象的屬性,再轉化為真實的對象類型。
JSON代表JavaScript對象符號。它是一個獨立的數據交換格式,是XML的最佳替代品。本章介紹了如何解析JSON文件,並從中提取所需的信息。Android提供了四個
可以顯示在的Android任務,通過加載進度條的進展。進度條有兩種形狀。加載欄和加載微調(spinner)。在本章中,我們將討論微調(spinner)。Spinner 用
繼前一篇文章講到Android上的SQLite分頁讀取,其功能只是用文本框顯示數據而已。本文就講得更加深入些,實現並封裝一個SQL分頁表格控件,不僅支持分頁還是以
經過一段時間收集了大量反饋意見後,我認為應該來說說這個話題了。我會在這裡給出我認為構建現代移動應用