深刻理解JNI

深刻理解JNI

本章主要內容html

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

本章涉及的源代碼文件名稱及位置android

如下是本章分析的源代碼文件名稱及其位置。數據庫

·  MediaScanner.java數組

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

·  android_media_MediaScanner.cpp函數

framework/base/media/jni/MediaScanner.cpp工具

·  android_media_MediaPlayer.cpppost

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的keyword,表示它將由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的keywordnative,它表示這兩個函數將由JNI層來實現。

Java層的分析到此結束。

JNI技術也很是照應Java程序猿。僅僅要完畢如下兩項工做就可以使用JNI了,它們是:

·  載入相應的JNI庫。

·  聲明由keywordnative修飾的函數。

因此對於Java程序猿來講。使用JNI技術真的是太easy了。只是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函數的。事實上。過程很是easy:

·  當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)基本類型的轉換

基本類型的轉換很是easy。可用表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層數據類型相應的轉換關係。很是easy。只是,應務必注意,轉換成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函數。

儘管函數簽名信息很是easy寫錯。但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,使用完後記住釋放它就可以了。這一點很是easy理解。如下要講有關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的權威指南。

相關文章
相關標籤/搜索