1. 程式人生 > >[深入理解Android卷一 全文-第二章]深入理解JNI

[深入理解Android卷一 全文-第二章]深入理解JNI

由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容。


第2章  深入理解JNI

本章主要內容

·  通過一個例項,介紹JNI技術和在使用中應注意的問題。

本章涉及的原始碼檔名及位置

下面是本章分析的原始碼檔名及其位置。

·  MediaScanner.java

framework/base/media/java/src/android/media/MediaScanner.java

·  android_media_MediaScanner.cpp

framework/base/media/jni/MediaScanner.cpp

·  android_media_MediaPlayer.cpp

framework/base/media/jni/android_media_MediaPlayer.cpp

·  AndroidRunTime.cpp

framework/base/core/jni/AndroidRunTime.cpp

·  JNIHelp.c

dalvik/libnativehelper/JNIHelp.c

2.1  概述

JNI,是Java Native Interface的縮寫,中文為Java本地呼叫。通俗地說,JNI是一種技術,通過這種技術可以做到以下兩點:

·  Java程式中的函式可以呼叫Native語言寫的函式,Native一般指的是C/C++編寫的函式。

·  Native程式中的函式可以呼叫Java層的函式,也就是在C/C++程式中可以呼叫Java的函式。

在平臺無關的Java中,為什麼要建立一個和Native相關的JNI技術呢?這豈不是破壞了Java的平臺無關特性嗎?本人覺得,JNI技術的推出可能是出於以下幾個方面的考慮:

·  承載Java世界的虛擬機器是用Native語言寫的,而虛擬機器又執行在具體平臺上,所以虛擬機器本身無法做到平臺無關。然而,有了JNI技術,就可以對Java層遮蔽具體的虛擬機器實現上的差異了。這樣,就能實現Java本身的平臺無關特性。其實Java一直在使用JNI技術,只是我們平時較少用到罷了。

·  早在Java語言誕生前,很多程式都是用Native語言寫的,它們遍佈在軟體世界的各個角落。Java出世後,它受到了追捧,並迅速得到發展,但仍無法對軟體世界徹底改朝換代,於是才有了折中的辦法。既然已經有Native模組實現了相關功能,那麼在Java中通過JNI技術直接使用它們就行了,免得落下重複製造輪子的壞名聲。另外,在一些要求效率和速度的場合還是需要Native語言參與的。

在Android平臺上,JNI就是一座將Native世界和Java世界間的天塹變成通途的橋,來看圖2-1,它展示了Android平臺上JNI所處的位置:


圖2-1  Android平臺中JNI示意圖

由上圖可知,JNI將Java世界和Native世界緊密地聯絡在一起了。在Android平臺上盡情使用Java開發的程式設計師們不要忘了,如果沒有JNI的支援,我們將寸步難行!

注意,雖然JNI層的程式碼是用Native語言寫的,但本書還是把和JNI相關的模組單獨歸類到JNI層。

俗話說,百聞不如一見,就來見識一下JNI技術吧。

2.2  通過例項學習JNI

初次接觸JNI,感覺最神奇的就是,Java竟然能夠呼叫Native的函式,可它是怎麼做到的呢?網上有很多介紹JNI的資料。由於Android大量使用了JNI技術,本節就將通過原始碼中的一處例項,來學習相關的知識,並瞭解它是如何呼叫Native的函式的。

這個例子,是和MediaScanner相關的。在本書的最後一章,會詳細分析它的工作原理,這裡先看和JNI相關的部分,如圖2-2所示:


圖2-2  MediaScanner和它的JNI

將圖2-2與圖2-1結合來看,可以知道:

·  Java世界對應的是MediaScanner,而這個MediaScanner類有一些函式是需要由Native層實現的。

·  JNI層對應的是libmedia_jni.so。media_jni是JNI庫的名字,其中,下劃線前的“media”是Native層庫的名字,這裡就是libmedia庫。下劃線後的”jni“表示它是一個JNI庫。注意,JNI庫的名字可以隨便取,不過Android平臺基本上都採用“lib模組名_jni.so”的命名方式。

·  Native層對應的是libmedia.so,這個庫完成了實際的功能。

·  MediaScanner將通過JNI庫libmedia_jni.so和Native的libmedia.so互動。

從上面的分析中還可知道:

·  JNI層必須實現為動態庫的形式,這樣Java虛擬機器才能載入它並呼叫它的函式。

下面來看MediaScanner。

MediaScanner是Android平臺中多媒體系統的重要組成部分,它的功能是掃描媒體檔案,得到諸如歌曲時長、歌曲作者等媒體資訊,並將它們存入到媒體資料庫中,供其他應用程式使用。

2.2.1  Java層的MediaScanner分析

來看MediaScanner(簡稱MS)的原始碼,這裡將提取出和JNI有關的部分,其程式碼如下所示:

[-->MediaScanner.java]

public class MediaScanner

{

static{ static語句

    /*

①載入對應的JNI庫,media_jni是JNI庫的名字。實際載入動態庫的時候會拓展成

libmedia_jni.so,在Windows平臺上將拓展為media_jni.dll。

*/

       System.loadLibrary("media_jni");

       native_init();//呼叫native_init函式

    }

.......

//非native函式

publicvoid scanDirectories(String[] directories, String volumeName){

  ......

}

//②宣告一個native函式。native為Java的關鍵字,表示它將由JNI層完成。

privatestatic native final void native_init();

    ......

privatenative void processFile(String path, String mimeType,

 MediaScannerClient client);

    ......

}

·  上面程式碼中列出了兩個比較重要的要點:

1. 載入JNI庫

前面說過,如Java要呼叫Native函式,就必須通過一個位於JNI層的動態庫才能做到。顧名思義,動態庫就是執行時載入的庫,那麼是什麼時候,在什麼地方載入這個庫呢?

這個問題沒有標準答案,原則上是在呼叫native函式前,任何時候、任何地方載入都可以。通行的做法是,在類的static語句中載入,通過呼叫System.loadLibrary方法就可以了。這一點,在上面的程式碼中也見到了,我們以後就按這種方法編寫程式碼即可。另外,System.loadLibrary函式的引數是動態庫的名字,即media_jni。系統會自動根據不同的平臺拓展成真實的動態庫檔名,例如在Linux系統上會拓展成libmedia_jni.so,而在Windows平臺上則會拓展成media_jni.dll。

解決了JNI庫載入的問題,再來來看第二個關鍵點。

2.  Java的native函式和總結

從上面程式碼中可以發現,native_init和processFile函式前都有Java的關鍵字native,它表示這兩個函式將由JNI層來實現。

Java層的分析到此結束。JNI技術也很照顧Java程式設計師,只要完成下面兩項工作就可以使用JNI了,它們是:

·  載入對應的JNI庫。

·  宣告由關鍵字native修飾的函式。

所以對於Java程式設計師來說,使用JNI技術真的是太容易了。不過JNI層可沒這麼輕鬆,下面來看MS的JNI層分析。

2.2.2  JNI層的MediaScanner分析

MS的JNI層程式碼在android_media_MediaScanner.cpp中,如下所示:

[-->android_media_MediaScanner.cpp]

//①這個函式是native_init的JNI層實現。

static void android_media_MediaScanner_native_init(JNIEnv *env)

{

    jclass clazz;

    clazz= env->FindClass("android/media/MediaScanner");

    ......

   fields.context = env->GetFieldID(clazz, "mNativeContext","I");

......

return;

}

//這個函式是processFile的JNI層實現。

static void android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz,

jstring path, jstring mimeType, jobject client)

{

    MediaScanner*mp = (MediaScanner *)env->GetIntField(thiz, fields.context);

    ......

    constchar *pathStr = env->GetStringUTFChars(path, NULL);

    ......

    if(mimeType) {

       env->ReleaseStringUTFChars(mimeType, mimeTypeStr);

    }

}

上面是MS的JNI層程式碼,不知道讀者看了以後是否會產生些疑惑?

我想,最大的疑惑可能是,怎麼會知道Java層的native_init函式對應的是JNI層的android_media_MediaScanner_native_init函式呢?下面就來回答這個問題。

1.   註冊JNI函式

正如程式碼中註釋的那樣,native_init函式對應的JNI函式是android_media_MediaScanner_native_init,可是細心的讀者可能要問了,你怎麼知道native_init函式對應的是這個android_media_MediaScanner_native_init,而不是其他的呢?莫非是根據函式的名字?

大家知道,native_init函式位於android.media這個包中,它的全路徑名應該是android.media.MediaScanner.native_init,而JNI層函式的名字是android_media_MediaScanner_native_init。因為在Native語言中,符號“.”有著特殊的意義,所以JNI層需要把“.”換成“_”。也就是通過這種方式,native_init找到了自己JNI層的本家兄弟android.media.MediaScanner.native_init。

上面的問題其實討論的是JNI函式的註冊問題,“註冊”之意就是將Java層的native函式和JNI層對應的實現函式關聯起來,有了這種關聯,呼叫Java層的native函式時,就能順利轉到JNI層對應的函式執行了。而JNI函式的註冊實際上有兩種方法,下面分別做介紹。

(1)靜態方法

我們從網上找到的與JNI有的關資料,一般都會介紹如何使用這種方法完成JNI函式的註冊,這種方法就是根據函式名來找對應的JNI函式。這種方法需要Java的工具程式javah參與,整體流程如下:

·  先編寫Java程式碼,然後編譯生成.class檔案。

·  使用Java的工具程式javah,如javah–o output packagename.classname ,這樣它會生成一個叫output.h的JNI層標頭檔案。其中packagename.classname是Java程式碼編譯後的class檔案,而在生成的output.h檔案裡,聲明瞭對應的JNI層函式,只要實現裡面的函式即可。

這個標頭檔案的名字一般都會使用packagename_class.h的樣式,例如MediaScanner對應的JNI層標頭檔案就是android_media_MediaScanner.h。下面,來看這種方式生成的標頭檔案:

[-->android_media_MediaScanner.h::樣例檔案]

/* DO NOT EDIT THIS FILE - it is machinegenerated */

#include <jni.h>  //必須包含這個標頭檔案,否則編譯通不過

/* Header for class android_media_MediaScanner*/

#ifndef _Included_android_media_MediaScanner

#define _Included_android_media_MediaScanner

#ifdef __cplusplus

extern "C" {

#endif

...... 略去一部分註釋內容

//processFile的JNI函式

JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile

                   (JNIEnv *, jobject, jstring,jstring, jobject);

......//略去一部分註釋內容

//native_init對應的JNI函式

JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init

  (JNIEnv*, jclass);

#ifdef __cplusplus

}

#endif

#endif

從上面程式碼中可以發現,native_init和processFile的JNI層函式被宣告成:

//Java層函式名中如果有一個”_”的話,轉換成JNI後就變成了”_l”。

JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init

JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile

需解釋一下,靜態方法中native函式是如何找到對應的JNI函式的。其實,過程非常簡單:

·  當Java層呼叫native_init函式時,它會從對應的JNI庫Java_android_media_MediaScanner_native_linit,如果沒有,就會報錯。如果找到,則會為這個native_init和Java_android_media_MediaScanner_native_linit建立一個關聯關係,其實就是儲存JNI層函式的函式指標。以後再呼叫native_init函式時,直接使用這個函式指標就可以了,當然這項工作是由虛擬機器完成的。

從這裡可以看出,靜態方法就是根據函式名來建立Java函式和JNI函式之間的關聯關係的,它要求JNI層函式的名字必須遵循特定的格式。這種方法也有幾個弊端,它們是:

·  需要編譯所有聲明瞭native函式的Java類,每個生成的class檔案都得用javah生成一個頭檔案。

·  javah生成的JNI層函式名特別長,書寫起來很不方便。

·  初次呼叫native函式時要根據函式名字搜尋對應的JNI層函式來建立關聯關係,這樣會影響執行效率。

有什麼辦法可以克服上面三種弊端嗎?根據上面的介紹,Java native函式是通過函式指標來和JNI層函式建立關聯關係的。如果直接讓native函式知道JNI層對應函式的函式指標,不就萬事大吉了嗎?這就是下面要介紹的第二種方法:動態註冊法。

(2)動態註冊

既然Java native函式數和JNI函式是一一對應的,那麼是不是會有一個結構來儲存這種關聯關係呢?答案是肯定的。在JNI技術中,用來記錄這種一一對應關係的,是一個叫JNINativeMethod的結構,其定義如下:

typedef struct {

   //Java中native函式的名字,不用攜帶包的路徑。例如“native_init“。

constchar* name;    

//Java函式的簽名信息,用字串表示,是引數型別和返回值型別的組合。

    const char* signature;

   void*       fnPtr;  //JNI層對應函式的函式指標,注意它是void*型別。

} JNINativeMethod;

應該如何使用這個結構體呢?來看MediaScanner JNI層是如何做的,程式碼如下所示:

[-->android_media_MediaScanner.cpp]

//定義一個JNINativeMethod陣列,其成員就是MS中所有native函式的一一對應關係。

static JNINativeMethod gMethods[] = {

    ......

{

"processFile" //Java中native函式的函式名。

//processFile的簽名信息,簽名信息的知識,後面再做介紹。

"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",   

 (void*)android_media_MediaScanner_processFile //JNI層對應函式指標。

},

 ......

{

"native_init",       

"()V",                     

(void *)android_media_MediaScanner_native_init

},

  ......

};

//註冊JNINativeMethod陣列

int register_android_media_MediaScanner(JNIEnv*env)

{

   //呼叫AndroidRuntime的registerNativeMethods函式,第二個引數表明是Java中的哪個類

    returnAndroidRuntime::registerNativeMethods(env,

               "android/media/MediaScanner", gMethods, NELEM(gMethods));

}

AndroidRunTime類提供了一個registerNativeMethods函式來完成註冊工作,下面看registerNativeMethods的實現,程式碼如下:

[-->AndroidRunTime.cpp]

int AndroidRuntime::registerNativeMethods(JNIEnv*env,

    constchar* className, const JNINativeMethod* gMethods, int numMethods)

{

    //呼叫jniRegisterNativeMethods函式完成註冊

    returnjniRegisterNativeMethods(env, className, gMethods, numMethods);

}

其中jniRegisterNativeMethods是Android平臺中,為了方便JNI使用而提供的一個幫助函式,其程式碼如下所示:

[-->JNIHelp.c]

int jniRegisterNativeMethods(JNIEnv* env, constchar* className,

                                  constJNINativeMethod* gMethods, int numMethods)

{

    jclassclazz;

    clazz= (*env)->FindClass(env, className);

......

//實際上是呼叫JNIEnv的RegisterNatives函式完成註冊的

    if((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {

       return -1;

    }

    return0;

}

wow,好像很麻煩啊!其實動態註冊的工作,只用兩個函式就能完成。總結如下:

/*

env指向一個JNIEnv結構體,它非常重要,後面會討論它。classname為對應的Java類名,由於

JNINativeMethod中使用的函式名並非全路徑名,所以要指明是哪個類。

*/

jclass clazz =  (*env)->FindClass(env, className);

//呼叫JNIEnv的RegisterNatives函式,註冊關聯關係。

(*env)->RegisterNatives(env, clazz, gMethods,numMethods);

所以,在自己的JNI層程式碼中使用這種方法,就可以完成動態註冊了。這裡還有一個很棘手的問題:這些動態註冊的函式在什麼時候、什麼地方被誰呼叫呢?好了,不賣關子了,直接給出該問題的答案:

·  當Java層通過System.loadLibrary載入完JNI動態庫後,緊接著會查詢該庫中一個叫JNI_OnLoad的函式,如果有,就呼叫它,而動態註冊的工作就是在這裡完成的。

所以,如果想使用動態註冊方法,就必須要實現JNI_OnLoad函式,只有在這個函式中,才有機會完成動態註冊的工作。靜態註冊則沒有這個要求,可我建議讀者也實現這個JNI_OnLoad函式,因為有一些初始化工作是可以在這裡做的。

那麼,libmedia_jni.so的JNI_OnLoad函式是在哪裡實現的呢?由於多媒體系統很多地方都使用了JNI,所以碼農把它放到android_media_MediaPlayer.cpp中了,程式碼如下所示:

[-->android_media_MediaPlayer.cpp]

jint JNI_OnLoad(JavaVM* vm, void* reserved)

{

   //該函式的第一個引數型別為JavaVM,這可是虛擬機器在JNI層的代表喔,每個Java程序只有一個

  //這樣的JavaVM

   JNIEnv* env = NULL;

    jintresult = -1;

    if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {

         gotobail;

    }

    ...... //動態註冊MediaScanner的JNI函式。

    if(register_android_media_MediaScanner(env) < 0) {

        goto bail;

}

......

returnJNI_VERSION_1_4;//必須返回這個值,否則會報錯。

}

JNI函式註冊的內容介紹完了。下面來關注JNI技術中其他的幾個重要部分。

JNI層程式碼中一般要包含jni.h這個標頭檔案。Android原始碼中提供了一個幫助標頭檔案JNIHelp.h,它內部其實就包含了jni.h,所以我們在自己的程式碼中直接包含這個JNIHelp.h即可。

2. 資料型別轉換

通過前面的分析,解決了JNI函式的註冊問題。下面來研究資料型別轉換的問題。

在Java中呼叫native函式傳遞的引數是Java資料型別,那麼這些引數型別到了JNI層會變成什麼呢?

Java資料型別分為基本資料型別和引用資料型別兩種,JNI層也是區別對待這二者的。先來看基本資料型別的轉換。

(1)基本型別的轉換

基本型別的轉換很簡單,可用表2-1表示:

表2-1  基本資料型別轉換關係表

Java

Native型別

符號屬性

字長

boolean

jboolean

無符號

8位

byte

jbyte

無符號

8位

char

jchar

無符號

16位

short

jshort

有符號

16位

int

jint

有符號

32位

long

jlong

有符號

64位

float

jfloat

有符號

32位

double

jdouble

有符號

64位

上面列出了Java基本資料型別和JNI層資料型別對應的轉換關係,非常簡單。不過,應務必注意,轉換成Native型別後對應資料型別的字長,例如jchar在Native語言中是16位,佔兩個位元組,這和普通的char佔一個位元組的情況完全不一樣。

接下來看Java引用資料型別的轉換。

(2)引用資料型別的轉換

引用資料型別的轉換如表2-2所示:

表2-2  Java引用資料型別轉換關係表

Java引用型別

Native型別

Java引用型別

Native型別

All objects

jobject

char[]

jcharArray

java.lang.Class例項

jclass

short[]

jshortArray

java.lang.String例項

jstring

int[]

jintArray

Object[]

jobjectArray

long[]

jlongArray

boolean[]

jbooleanArray

float[]

floatArray

byte[]

jbyteArray

double[]

jdoubleArray

java.lang.Throwable例項

jthrowable

由上表可知:

·  除了Java中基本資料型別的陣列、Class、String和Throwable外,其餘所有Java物件的資料型別在JNI中都用jobject表示。

這一點太讓人驚訝了!看processFile這個函式:

//Java層processFile有三個引數。

processFile(String path, StringmimeType,MediaScannerClient client);

//JNI層對應的函式,最後三個引數和processFile的引數對應。

android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz,

jstring path, jstring mimeType, jobject client)

從上面這段程式碼中可以發現:

·  Java的String型別在JNI層對應為jstring。

·  Java的MediaScannerClient型別在JNI層對應為jobject。

如果物件型別都用jobject表示,就好比是Native層的void*型別一樣,對碼農來說,是完全透明的。既然是透明的,那該如何使用和操作它們呢?在回答這個問題之前,再來仔細看看上面那個android_media_MediaScanner_processFile函式,程式碼如下:

/*

Java中的processFile只有三個引數,為什麼JNI層對應的函式會有五個引數呢?第一個引數中的JNIEnv是什麼?稍後介紹。第二個引數jobject代表Java層的MediaScanner物件,它表示

是在哪個MediaScanner物件上呼叫的processFile。如果Java層是static函式的話,那麼

這個引數將是jclass,表示是在呼叫哪個Java Class的靜態函式。

*/

android_media_MediaScanner_processFile(JNIEnv*env,

jobject thiz,

jstring path, jstring mimeType, jobject client)

上面的程式碼,引出了下面幾節的主角JNIEnv。

3. JNIEnv介紹

JNIEnv是一個和執行緒相關的,代表JNI環境的結構體,圖2-3展示了JNIEnv的內部結構:


圖2-3  JNIEnv內部結構簡圖

從上圖可知,JNIEnv實際上就是提供了一些JNI系統函式。通過這些函式可以做到:

·  呼叫Java的函式。

·  操作jobject物件等很多事情。

後面小節中將具體介紹怎麼使用JNIEnv中的函式。這裡,先介紹一個關於JNIEnv的重要知識點。

上面提到說JNIEnv,是一個和執行緒有關的變數。也就是說,執行緒A有一個JNIEnv,執行緒B有一個JNIEnv。由於執行緒相關,所以不能線上程B中使用執行緒A的JNIEnv結構體。讀者可能會問,JNIEnv不都是native函式轉換成JNI層函式後由虛擬機器傳進來的嗎?使用傳進來的這個JNIEnv總不會錯吧?是的,在這種情況下使用當然不會出錯。不過當後臺執行緒收到一個網路訊息,而又需要由Native層函式主動回撥Java層函式時,JNIEnv是從何而來呢?根據前面的介紹可知,我們不能儲存另外一個執行緒的JNIEnv結構體,然後把它放到後臺執行緒中來用。這該如何是好?

還記得前面介紹的那個JNI_OnLoad函式嗎?它的第一個引數是JavaVM,它是虛擬機器在JNI層的代表,程式碼如下所示:

//全程序只有一個JavaVM物件,所以可以儲存,任何地方使用都沒有問題。

jint JNI_OnLoad(JavaVM* vm, void* reserved)

正如上面程式碼所說,不論程序中有多少個執行緒,JavaVM卻是獨此一份,所以在任何地方都可以使用它。那麼,JavaVM和JNIEnv又有什麼關係呢?答案如下:

·  呼叫JavaVM的AttachCurrentThread函式,就可得到這個執行緒的JNIEnv結構體。這樣就可以在後臺執行緒中回撥Java函數了。

·  另外,後臺執行緒退出前,需要呼叫JavaVM的DetachCurrentThread函式來釋放對應的資源。

再來看JNIEnv的作用。

4. 通過JNIEnv操作jobject

前面提到過一個問題,即Java的引用型別除了少數幾個外,最終在JNI層都用jobject來表示物件的資料型別,那麼該如何操作這個jobject呢?

從另外一個角度來解釋這個問題。一個Java物件是由什麼組成的?當然是它的成員變數和成員函數了。那麼,操作jobject的本質就應當是操作這些物件的成員變數和成員函式。所以應先來看與成員變數及成員函式有關的內容。

(1)jfieldID 和jmethodID的介紹

我們知道,成員變數和成員函式是由類定義的,它是類的屬性,所以在JNI規則中,用jfieldID 和jmethodID 來表示Java類的成員變數和成員函式,它們通過JNIEnv的下面兩個函式可以得到:

jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);

jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);

其中,jclass代表Java類,name表示成員函式或成員變數的名字,sig為這個函式和變數的簽名信息。如前所示,成員函式和成員變數都是類的資訊,這兩個函式的第一個引數都是jclass。

MS中是怎麼使用它們的呢?來看程式碼,如下所示:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient建構函式]

 MyMediaScannerClient(JNIEnv *env, jobjectclient)......

{

 //先找到android.media.MediaScannerClient類在JNI層中對應的jclass例項。

jclass mediaScannerClientInterface =

env->FindClass("android/media/MediaScannerClient");

 //取出MediaScannerClient類中函式scanFile的jMethodID。

mScanFileMethodID = env->GetMethodID(

mediaScannerClientInterface, "scanFile",

                           "(Ljava/lang/String;JJ)V");

 //取出MediaScannerClient類中函式handleStringTag的jMethodID。

 mHandleStringTagMethodID = env->GetMethodID(

mediaScannerClientInterface,"handleStringTag",

                             "(Ljava/lang/String;Ljava/lang/String;)V");

  ......

}

在上面程式碼中,將scanFile和handleStringTag函式的jmethodID儲存為MyMediaScannerClient的成員變數。為什麼這裡要把它們儲存起來呢?這個問題涉及一個事關程式執行效率的知識點:

·  如果每次操作jobject前都去查詢jmethoID或jfieldID的話將會影響程式執行的效率。所以我們在初始化的時候,就可以取出這些ID並儲存起來以供後續使用。

取出jmethodID後,又該怎麼用它呢?

(2)使用jfieldID和jmethodID

下面再看一個例子,其程式碼如下所示:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile]

 virtualbool scanFile(const char* path, long long lastModified,

long long fileSize)

    {

       jstring pathStr;

        if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;

/*

呼叫JNIEnv的CallVoidMethod函式,注意CallVoidMethod的引數:

第一個是代表MediaScannerClient的jobject物件,

第二個引數是函式scanFile的jmethodID,後面是Java中scanFile的引數。

*/

       mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,

lastModified, fileSize);

       mEnv->DeleteLocalRef(pathStr);

       return (!mEnv->ExceptionCheck());

}

明白了,通過JNIEnv輸出的CallVoidMethod,再把jobject、jMethodID和對應引數傳進去,JNI層就能夠呼叫Java物件的函數了!

實際上JNIEnv輸出了一系列類似CallVoidMethod的函式,形式如下:

NativeType Call<type>Method(JNIEnv *env,jobject obj,jmethodID methodID, ...)。

其中type是對應Java函式的返回值型別,例如CallIntMethod、CallVoidMethod等。

上面是針對非static函式的,如果想呼叫Java中的static函式,則用JNIEnv輸出的CallStatic<Type>Method系列函式。

現在,我們已瞭解瞭如何通過JNIEnv操作jobject的成員函式,那麼怎麼通過jfieldID操作jobject的成員變數呢?這裡,直接給出整體解決方案,如下所示:

//獲得fieldID後,可呼叫Get<type>Field系列函式獲取jobject對應成員變數的值。

NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)

//或者呼叫Set<type>Field系列函式來設定jobject對應成員變數的值。

void Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)

//下面我們列出一些參加的Get/Set函式。

GetObjectField()         SetObjectField()

GetBooleanField()         SetBooleanField()

GetByteField()           SetByteField()

GetCharField()           SetCharField()

GetShortField()          SetShortField()

GetIntField()            SetIntField()

GetLongField()           SetLongField()

GetFloatField()          SetFloatField()

GetDoubleField()                  SetDoubleField()

通過本節的介紹,相信讀者已瞭解jfieldID和jmethodID的作用,也知道如何通過JNIEnv的函式來操作jobject了。雖然jobject是透明的,但有了JNIEnv的幫助,還是能輕鬆操作jobject背後的實際物件了。

5. jstring介紹

Java中的String也是引用型別,不過由於它的使用非常頻繁,所以在JNI規範中單獨建立了一個jstring型別來表示Java中的String型別。雖然jstring是一種獨立的資料型別,但是它並沒有提供成員函式供操作。相比而言,C++中的string類就有自己的成員函數了。那麼該怎麼操作jstring呢?還是得依靠JNIEnv提供的幫助。這裡看幾個有關jstring的函式:

·  呼叫JNIEnv的NewString(JNIEnv *env, const jchar*unicodeChars,jsize len),可以從Native的字串得到一個jstring物件。其實,可以把一個jstring物件看成是Java中String物件在JNI層的代表,也就是說,jstring就是一個Java String。但由於Java String儲存的是Unicode字串,所以NewString函式的引數也必須是Unicode字串。

·  呼叫JNIEnv的NewStringUTF將根據Native的一個UTF-8字串得到一個jstring物件。在實際工作中,這個函式用得最多。

·  上面兩個函式將本地字串轉換成了Java的String物件,JNIEnv還提供了GetStringChars和GetStringUTFChars函式,它們可以將Java String物件轉換成本地字串。其中GetStringChars得到一個Unicode字串,而GetStringUTFChars得到一個UTF-8字串。

·  另外,如果在程式碼中呼叫了上面幾個函式,在做完相關工作後,就都需要呼叫ReleaseStringChars或ReleaseStringUTFChars函式對應地釋放資源,否則會導致JVM記憶體洩露。這一點和jstring的內部實現有關係,讀者寫程式碼時務必注意這個問題。

為了加深印象,來看processFile是怎麼做的:

[-->android_media_MediaScanner.cpp]

static void

android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz, jstring path, jstring mimeType, jobject client)

{

   MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz,fields.context);

......

//呼叫JNIEnv的GetStringUTFChars得到本地字串pathStr

    constchar *pathStr = env->GetStringUTFChars(path, NULL);

......

//使用完後,必須呼叫ReleaseStringUTFChars釋放資源

   env->ReleaseStringUTFChars(path, pathStr);

    ......

}

6. JNI型別簽名的介紹

先來看動態註冊中的一段程式碼:

tatic JNINativeMethod gMethods[] = {

    ......

{

"processFile"

//processFile的簽名信息,這麼長的字串,是什麼意思?

"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",   

 (void*)android_media_MediaScanner_processFile

},

  ......

}

上面程式碼中的JNINativeMethod已經見過了,不過其中那個很長的字串"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V"是什麼意思呢?

根據前面的介紹可知,它是Java中對應函式的簽名信息,由引數型別和返回值型別共同組成。不過為什麼需要這個簽名信息呢?

·  這個問題的答案比較簡單。因為Java支援函式過載,也就是說,可以定義同名但不同引數的函式。但僅僅根據函式名,是沒法找到具體函式的。為了解決這個問題,JNI技術中就使用了引數型別和返回值型別的組合,作為一個函式的簽名信息,有了簽名信息和函式名,就能很順利地找到Java中的函數了。

JNI規範定義的函式簽名信息看起來很彆扭,不過習慣就好了。它的格式是:

(引數1型別標示引數2型別標示...引數n型別標示)返回值型別標示。

來看processFile的例子:

Java中函式定義為void processFile(String path, String mimeType)

對應的JNI函式簽名就是

(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V

 其中,括號內是引數型別的標示,最右邊是返回值型別的標示,void型別對應的標示是V。

 當引數的型別是引用型別時,其格式是”L包名;”,其中包名中的”.”換成”/”。上面例子中的

Ljava/lang/String;表示是一個Java String型別。

函式簽名不僅看起來麻煩,寫起來更麻煩,稍微寫錯一個標點就會導致註冊失敗。所以,在具體編碼時,讀者可以定義字串巨集,這樣改起來也方便。

表2-3是常見的型別標示:

表2-3  型別標示示意表

型別標示

Java型別

型別標示

Java型別

Z

boolean

F

float

B

byte

D

double

C

char

L/java/langaugeString;

String

S

short

[I

int[]

I

int

[L/java/lang/object;

Object[]

J

long

上面列出了一些常用的型別標示。請讀者注意,如果Java型別是陣列,則標示中會有一個“[”,另外,引用型別(除基本型別的陣列外)的標示最後都有一個“;”。

再來看一個小例子,如表2-4所示:

表2-4  函式簽名小例子

函式簽名

Java函式

“()Ljava/lang/String;”

String f()

“(ILjava/lang/Class;)J”

long f(int i, Class c)

“([B)V”

void f(byte[] bytes)

請讀者結合表2-3和表2-4左欄的內容寫出對應的Java函式。

雖然函式簽名信息很容易寫錯,但Java提供一個叫javap的工具能幫助生成函式或變數的簽名信息,它的用法如下:

javap –s -p xxx。其中xxx為編譯後的class檔案,s表示輸出內部資料型別的簽名信息,p表示列印所有函式和成員的簽名信息,而預設只會列印public成員和函式的簽名信息。

有了javap,就不用死記硬背上面的型別標示了。

7. 垃圾回收

我們知道,Java中建立的物件最後是由垃圾回收器來回收和釋放記憶體的,可它對JNI有什麼影響呢?下面看一個例子:

[-->垃圾回收例子]

static jobject save_thiz = NULL; //定義一個全域性的jobject

static void

android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz, jstring path,

 jstringmimeType, jobject client)

{

  ......

  //儲存Java層傳入的jobject物件,代表MediaScanner物件

save_thiz = thiz;

......

return;

}

//假設在某個時間,有地方呼叫callMediaScanner函式

void callMediaScanner()

{

  //在這個函式中操作save_thiz,會有問題嗎?

}

上面的做法肯定會有問題,因為和save_thiz對應的Java層中的MediaScanner很有可能已經被垃圾回收了,也就是說,save_thiz儲存的這個jobject可能是一個野指標,如使用它,後果會很嚴重。

可能有人要問,將一個引用型別進行賦值操作,它的引用計數不會增加嗎?而垃圾回收機制只會保證那些沒有被引用的物件才會被清理。問得對,但如果在JNI層使用下面這樣的語句,是不會增加引用計數的。

save_thiz = thiz; //這種賦值不會增加jobject的引用計數。

那該怎麼辦?不必擔心,JNI規範已很好地解決了這一問題,JNI技術一共提供了三種類型的引用,它們分別是:

·  Local Reference:本地引用。在JNI層函式中使用的非全域性引用物件都是Local Reference。它包括函式呼叫時傳入的jobject、在JNI層函式中建立的jobject。LocalReference最大的特點就是,一旦JNI層函式返回,這些jobject就可能被垃圾回收。

·  Global Reference:全域性引用,這種物件如不主動釋放,就永遠不會被垃圾回收。

·  Weak Global Reference:弱全域性引用,一種特殊的GlobalReference,在執行過程中可能會被垃圾回收。所以在程式中使用它之前,需要呼叫JNIEnv的IsSameObject判斷它是不是被回收了。

平時用得最多的是Local Reference和Global Reference,下面看一個例項,程式碼如下所示:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient建構函式]

 MyMediaScannerClient(JNIEnv *env, jobjectclient)

       :   mEnv(env),

        //呼叫NewGlobalRef建立一個GlobalReference,這樣mClient就不用擔心被回收了。

           mClient(env->NewGlobalRef(client)),

           mScanFileMethodID(0),

           mHandleStringTagMethodID(0),

           mSetMimeTypeMethodID(0)

{

  ......

}

//解構函式

virtual ~MyMediaScannerClient()

{

  mEnv->DeleteGlobalRef(mClient);//呼叫DeleteGlobalRef釋放這個全域性引用。

 }

每當JNI層想要儲存Java層中的某個物件時,就可以使用Global Reference,使用完後記住釋放它就可以了。這一點很容易理解。下面要講有關LocalReference的一個問題,還是先看例項,程式碼如下所示:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile]

 virtualbool scanFile(const char* path, long long lastModified,

long long fileSize)

{

   jstringpathStr;

   //呼叫NewStringUTF建立一個jstring物件,它是Local Reference型別。

   if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;

        //呼叫Java的scanFile函式,把這個jstring傳進去

       mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,

lastModified, fileSize);

     /*

      根據LocalReference的說明,這個函式返回後,pathStr物件就會被回收。所以

      下面這個DeleteLocalRef呼叫看起來是多餘的,其實不然,這裡解釋一下原因:

1)如果不呼叫DeleteLocalRef,pathStr將在函式返回後被回收。

2)如果呼叫DeleteLocalRef的話,pathStr會立即被回收。這兩者看起來沒什麼區別,

不過程式碼要是像下面這樣的話,虛擬機器的記憶體就會被很快被耗盡:

      for(inti = 0; i < 100; i++)

      {

           jstring pathStr = mEnv->NewStringUTF(path);

           ......//做一些操作

          //mEnv->DeleteLocalRef(pathStr); //不立即釋放Local Reference

}

如果在上面程式碼的迴圈中不呼叫DeleteLocalRef的話,則會建立100個jstring,

那麼記憶體的耗費就非常可觀了!

     */

   mEnv->DeleteLocalRef(pathStr);

   return(!mEnv->ExceptionCheck());

}

所以,沒有及時回收的Local Reference或許是程序佔用過多的一個原因,請務必注意這一點。

8. JNI中的異常處理

JNI中也有異常,不過它和C++、Java的異常不太一樣。當呼叫JNIEnv的某些函數出錯後,會產生一個異常,但這個異常不會中斷本地函式的執行,直到從JNI層返回到Java層後,虛擬機器才會丟擲這個異常。雖然在JNI層中產生的異常不會中斷本地函式的執行,但一旦產生異常後,就只能做一些資源清理工作了(例如釋放全域性引用,或者ReleaseStringChars)。如果這時呼叫除上面所說函式之外的其他JNIEnv函式,則會導致程式死掉。

來看一個和異常處理有關的例子,程式碼如下所示:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile函式]

 virtualbool scanFile(const char* path, long long lastModified,

long long fileSize)

 {

       jstring pathStr;

       //NewStringUTF呼叫失敗後,直接返回,不能再幹別的事情了。

        if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;

       ......

}

JNI層函式可以在程式碼中截獲和修改這些異常,JNIEnv提供了三個函式進行幫助:

·  ExceptionOccured函式,用來判斷是否發生異常。

·  ExceptionClear函式,用來清理當前JNI層中發生的異常。

·  ThrowNew函式,用來向Java層丟擲異常。

異常處理是JNI層程式碼必須關注的事情,讀者在編寫程式碼時務小心對待。

2.3  本章小結

本章通過一個例項介紹了JNI技術中的幾個重要方面,包括:

·  JNI函式註冊的方法。

·  Java和JNI層資料型別的轉換。

·  JNIEnv和jstring的使用方法,以及JNI中的型別簽名。

·  最後介紹了垃圾回收在JNI層中的使用,以及異常處理方面的知識。

相信掌握了上面的知識後,我們會對JNI技術有一個比較清晰的認識。這裡,還要建議讀者再認真閱讀一下JDK文件中的《Java Native Interface Specification》,它完整和細緻地闡述了JNI技術的各個方面,堪稱深入學習JNI的權威指南。

相關推薦

[深入理解Android 全文-第二]深入理解JNI

由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容。第2章  深入理解JNI本章主要內容·  通過一個例項,介紹JNI技術和在使用中應注意的問題。本章涉

[深入理解Android全文-第六]深入理解Binder

由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容。第6章 深入理解Binder本章主要內容·  以MediaServer為切入點,對Binder的工作

[深入理解Android全文-第三]深入理解init

由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容。第3章  深入理解init本章主要內容·  深入分析init。本章涉及的原始碼檔名及位置下面是本章分

深入理解AndroidIII 第7 深入理解SystemUI (節選)

 多謝華章圖書與鄧凡平先生的幫助,《深入理解Android卷III〉終於上市了。歡迎大家來這裡一起探討文中的問題或與Android系統有關的任何話題。 第7章深入理解SystemUI 本章主要內容: 探討狀態列與導航欄的啟動過程

[深入理解Android全文-第三]深入理解SystemServer

由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容 第3章  深入理解SystemServer本章主要內容:·  分析SystemServer·  分析

[深入理解Android全文-第五]深入理解PowerManagerService

由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容第5章  深入理解PowerManagerService本章主要內容:·  深入分析PowerMana

深入理解計算機系統筆記之第二()

資訊的表示和處理(一) 大多數計算機使用8位的塊(也就是一個位元組byte),由此可以看到32位(4個位元組)系統和64位(8個位元組)系統的區別。32位系統在於cpu可以同時處理4個位元組(32位)的資料,那麼64位系統cpu可以同時處理8個位元組(64位)的資料。 一個

深入理解Android III》第八深入理解Android桌布(節選)

                      第8章 深入理解Android桌布(節選) 本章主要內容: ·  討論動態桌布的實現。 ·  在動態桌布的基礎上討論靜態桌布的實現。 ·  討論WMS對桌布視窗所做的特殊處理。 本章涉及的原始碼檔名及位置: ·  Wal

深入理解Android III》第四 深入理解WindowManagerService

《深入理解Android 卷III》即將釋出,作者是張大偉。此書填補了深入理解Android Framework卷中的一個主要空白,即Android Framework中和UI相關的部分。在一個特別講究顏值的時代,本書分析了Android 4.2中WindowManagerS

深入理解Android III》第五 深入理解Android輸入系統

《深入理解Android 卷III》即將釋出,作者是張大偉。此書填補了深入理解Android Framework卷中的一個主要空白,即Android Framework中和UI相關的部分。在一個特別講究顏值的時代,本書分析了Android 4.2中WindowManagerS

[讀書筆記][第二] 深入理解C# -- C# in depth

ch2 C#1所搭建的核心基礎 委託 宣告委託 方法執行程式碼:相容的方法簽名 建立委託例項 呼叫例項:Invoke() 或簡化呼叫 加減委託 呼叫列表,Combine() + , Remove() - 事件 事件是委託型別,是屬性,封裝了publish

深入理解Android III》推薦序

轉載:https://blog.csdn.net/innost/article/details/47292791《深入理解Android 卷III》即將釋出,作者是張大偉。此書填補了深入理解Android Framework卷中的一個主要空白,即Android Framewo

JNI(深入理解AndroidI)的讀書筆記

一:概述 JNI:Java Native Interface。 作用:連線Java世界和Native世界。Java程式中函式可以呼叫Native語言寫的函式;Native程式中的函式可以呼叫Java層的函式。 二:例項:MediaScanner 2.1 關係: Java層(

深入理解android(1)pdf

下載地址:網盤下載一本以情景方式對Android的原始碼進行深入分析的書。內容廣泛,以對Framework層的分析為主,兼顧Native層和Application層;分析深入,每一部分原始碼的分析都力求透徹;針對性強,注重實際應用開發需求,書中所涵蓋的知識點都是Android

深入理解 Android I

原文地址:http://wiki.jikexueyuan.com/project/deep-android-v1/ 第8章  深入理解Surface系統 本章主要內容 ·  詳細分析一個Activity的顯示過程。 ·  詳細分析Surface

深入理解Android):Gradle詳解

作者 鄧凡平 編者按:隨著移動裝置硬體能力的提升,Android系統開放的特質開始顯現,各種開發的奇技淫巧、黑科技不斷湧現,InfoQ特聯合《深入理解Android》系列圖書作者鄧凡平,開設深入理解Android專欄,探索Android從框架到應用開

深入理解java虛擬機器》第二筆記

1. 執行時資料區域 名稱 是否共享 作用 存在的異常 程式計數器 執行緒私有 如果執行的是java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址 java虛擬機器棧 執行緒私有 每個

深入理解Android之Java Security第二部分(Final)

深入理解Android之Java Security(第二部分,最後)程式碼路徑:Security.java:libcore/lunl/src/main/java/java/security/TrustedCertificateStore.java:libcore /crypt

《第一行程式碼Android》學習總結第二 Activity建立與相關設定

一、id標籤 如果在XML檔案中引用一個id,則使用@id/id_name; 如果在XML檔案中定義一個id,則使用@+id/id_name。 二、程式中設定主活動 在AndroidMaifest.xml中設定 <intent-filter>   

第二理解DispatcherServlet ——深入淺出學Spring Web MVC

整合Web環境的通用配置: <context-param>       <param-name>contextConfigLocation</param-name>       <param-value>           classpath:spring-