1. 程式人生 > >Android系統移植與平臺開發(九)- JNI介紹

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程式碼呼叫本地方法時,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介面呼叫時,要在內部都要轉換,我們在使用的時候也需要小心。

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, jobjectArrayarrayjsize index)/ SetObjectArrayElement (JNIEnv *env, jobjectArrayarrayjsize index, jobject val)這樣的函式來對javaObject[]陣列進行操作。

注意:使用上述的函式也不用釋放任何資源。


 

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物件的屬性,再轉化為真實的物件型別。