在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ Android/ 第2章? 深入理解JNI
第4章 ?深入理解 Zygote
第10章 深入理解MediaScanner
第3章 ?深入理解init
第8章 ?深入理解Surface系統(tǒng)
第5章 深入理解常見類
第7章 ?深入理解Audio系統(tǒng)
第一章 ?閱讀前的準(zhǔn)備工作
<span>第6章 深入理解Binder</span>
第9章 ?深入理解Vold和Rild
第2章? 深入理解JNI

第2章? 深入理解JNI

本章主要內(nèi)容

·? 通過一個實例,介紹JNI技術(shù)和在使用中應(yīng)注意的問題。

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

下面是本章分析的源碼文件名及其位置。

·? 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本地調(diào)用。通俗地說,JNI是一種技術(shù),通過這種技術(shù)可以做到以下兩點:

·? Java程序中的函數(shù)可以調(diào)用Native語言寫的函數(shù),Native一般指的是C/C++編寫的函數(shù)。

·? Native程序中的函數(shù)可以調(diào)用Java層的函數(shù),也就是在C/C++程序中可以調(diào)用Java的函數(shù)。

在平臺無關(guān)的Java中,為什么要創(chuàng)建一個和Native相關(guān)的JNI技術(shù)呢?這豈不是破壞了Java的平臺無關(guān)特性嗎?本人覺得,JNI技術(shù)的推出可能是出于以下幾個方面的考慮:

·? 承載Java世界的虛擬機(jī)是用Native語言寫的,而虛擬機(jī)又運(yùn)行在具體平臺上,所以虛擬機(jī)本身無法做到平臺無關(guān)。然而,有了JNI技術(shù),就可以對Java層屏蔽具體的虛擬機(jī)實現(xiàn)上的差異了。這樣,就能實現(xiàn)Java本身的平臺無關(guān)特性。其實Java一直在使用JNI技術(shù),只是我們平時較少用到罷了。

·? 早在Java語言誕生前,很多程序都是用Native語言寫的,它們遍布在軟件世界的各個角落。Java出世后,它受到了追捧,并迅速得到發(fā)展,但仍無法對軟件世界徹底改朝換代,于是才有了折中的辦法。既然已經(jīng)有Native模塊實現(xiàn)了相關(guān)功能,那么在Java中通過JNI技術(shù)直接使用它們就行了,免得落下重復(fù)制造輪子的壞名聲。另外,在一些要求效率和速度的場合還是需要Native語言參與的。

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

http://wiki.jikexueyuan.com/project/deep-android-v1/images/chapter2/image001.png" alt="image" />

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

由上圖可知,JNI將Java世界和Native世界緊密地聯(lián)系在一起了。在Android平臺上盡情使用Java開發(fā)的程序員們不要忘了,如果沒有JNI的支持,我們將寸步難行!

注意,雖然JNI層的代碼是用Native語言寫的,但本書還是把和JNI相關(guān)的模塊單獨歸類到JNI層。

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

?

2.2 ?通過實例學(xué)習(xí)JNI

初次接觸JNI,感覺最神奇的就是,Java竟然能夠調(diào)用Native的函數(shù),可它是怎么做到的呢?網(wǎng)上有很多介紹JNI的資料。由于Android大量使用了JNI技術(shù),本節(jié)就將通過源碼中的一處實例,來學(xué)習(xí)相關(guān)的知識,并了解它是如何調(diào)用Native的函數(shù)的。

這個例子,是和MediaScanner相關(guān)的。在本書的最后一章,會詳細(xì)分析它的工作原理,這里先看和JNI相關(guān)的部分,如圖2-2所示:

http://wiki.jikexueyuan.com/project/deep-android-v1/images/chapter2/image002.png" alt="image" />

圖2-2? MediaScanner和它的JNI

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

·? Java世界對應(yīng)的是MediaScanner,而這個MediaScanner類有一些函數(shù)是需要由Native層實現(xiàn)的。

·? JNI層對應(yīng)的是libmedia_jni.so。media_jni是JNI庫的名字,其中,下劃線前的“media”是Native層庫的名字,這里就是libmedia庫。下劃線后的”jni“表示它是一個JNI庫。注意,JNI庫的名字可以隨便取,不過Android平臺基本上都采用“l(fā)ib模塊名_jni.so”的命名方式。

·? Native層對應(yīng)的是libmedia.so,這個庫完成了實際的功能。

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

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

·? JNI層必須實現(xiàn)為動態(tài)庫的形式,這樣Java虛擬機(jī)才能加載它并調(diào)用它的函數(shù)。

下面來看MediaScanner。

MediaScanner是Android平臺中多媒體系統(tǒng)的重要組成部分,它的功能是掃描媒體文件,得到諸如歌曲時長、歌曲作者等媒體信息,并將它們存入到媒體數(shù)據(jù)庫中,供其他應(yīng)用程序使用。

2.2.1 ?Java層的MediaScanner分析

來看MediaScanner(簡稱MS)的源碼,這里將提取出和JNI有關(guān)的部分,其代碼如下所示:

[-->MediaScanner.java]

public class MediaScanner

{

static{ static語句

??? /*

①加載對應(yīng)的JNI庫,media_jni是JNI庫的名字。實際加載動態(tài)庫的時候會拓展成

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

*/

???????System.loadLibrary("media_jni");

???????native_init();//調(diào)用native_init函數(shù)

??? }

.......

//非native函數(shù)

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

? ......

}

?

//②聲明一個native函數(shù)。native為Java的關(guān)鍵字,表示它將由JNI層完成。

privatestatic native final void native_init();

??? ......

privatenative void processFile(String path, String mimeType,

?MediaScannerClient client);

??? ......

}

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

1. 加載JNI庫

前面說過,如Java要調(diào)用Native函數(shù),就必須通過一個位于JNI層的動態(tài)庫才能做到。顧名思義,動態(tài)庫就是運(yùn)行時加載的庫,那么是什么時候,在什么地方加載這個庫呢?

這個問題沒有標(biāo)準(zhǔn)答案,原則上是在調(diào)用native函數(shù)前,任何時候、任何地方加載都可以。通行的做法是,在類的static語句中加載,通過調(diào)用System.loadLibrary方法就可以了。這一點,在上面的代碼中也見到了,我們以后就按這種方法編寫代碼即可。另外,System.loadLibrary函數(shù)的參數(shù)是動態(tài)庫的名字,即media_jni。系統(tǒng)會自動根據(jù)不同的平臺拓展成真實的動態(tài)庫文件名,例如在Linux系統(tǒng)上會拓展成libmedia_jni.so,而在Windows平臺上則會拓展成media_jni.dll。

解決了JNI庫加載的問題,再來來看第二個關(guān)鍵點。

2.? Java的native函數(shù)和總結(jié)

從上面代碼中可以發(fā)現(xiàn),native_init和processFile函數(shù)前都有Java的關(guān)鍵字native,它表示這兩個函數(shù)將由JNI層來實現(xiàn)。

Java層的分析到此結(jié)束。JNI技術(shù)也很照顧Java程序員,只要完成下面兩項工作就可以使用JNI了,它們是:

·? 加載對應(yīng)的JNI庫。

·? 聲明由關(guān)鍵字native修飾的函數(shù)。

所以對于Java程序員來說,使用JNI技術(shù)真的是太容易了。不過JNI層可沒這么輕松,下面來看MS的JNI層分析。

2.2.2 ?JNI層的MediaScanner分析

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

[-->android_media_MediaScanner.cpp]

//①這個函數(shù)是native_init的JNI層實現(xiàn)。

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;

}

?

//這個函數(shù)是processFile的JNI層實現(xiàn)。

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層代碼,不知道讀者看了以后是否會產(chǎn)生些疑惑?

我想,最大的疑惑可能是,怎么會知道Java層的native_init函數(shù)對應(yīng)的是JNI層的android_media_MediaScanner_native_init函數(shù)呢?下面就來回答這個問題。

1.???注冊JNI函數(shù)

正如代碼中注釋的那樣,native_init函數(shù)對應(yīng)的JNI函數(shù)是android_media_MediaScanner_native_init,可是細(xì)心的讀者可能要問了,你怎么知道native_init函數(shù)對應(yīng)的是這個android_media_MediaScanner_native_init,而不是其他的呢?莫非是根據(jù)函數(shù)的名字?

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

上面的問題其實討論的是JNI函數(shù)的注冊問題,“注冊”之意就是將Java層的native函數(shù)和JNI層對應(yīng)的實現(xiàn)函數(shù)關(guān)聯(lián)起來,有了這種關(guān)聯(lián),調(diào)用Java層的native函數(shù)時,就能順利轉(zhuǎn)到JNI層對應(yīng)的函數(shù)執(zhí)行了。而JNI函數(shù)的注冊實際上有兩種方法,下面分別做介紹。

(1)靜態(tài)方法

我們從網(wǎng)上找到的與JNI有的關(guān)資料,一般都會介紹如何使用這種方法完成JNI函數(shù)的注冊,這種方法就是根據(jù)函數(shù)名來找對應(yīng)的JNI函數(shù)。這種方法需要Java的工具程序javah參與,整體流程如下:

·? 先編寫Java代碼,然后編譯生成.class文件。

·? 使用Java的工具程序javah,如javah–o output packagename.classname ,這樣它會生成一個叫output.h的JNI層頭文件。其中packagename.classname是Java代碼編譯后的class文件,而在生成的output.h文件里,聲明了對應(yīng)的JNI層函數(shù),只要實現(xiàn)里面的函數(shù)即可。

這個頭文件的名字一般都會使用packagename_class.h的樣式,例如MediaScanner對應(yīng)的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

...... 略去一部分注釋內(nèi)容

//processFile的JNI函數(shù)

JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile

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

?

......//略去一部分注釋內(nèi)容

//native_init對應(yīng)的JNI函數(shù)

JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init

? (JNIEnv*, jclass);

?

#ifdef __cplusplus

}

#endif

#endif

從上面代碼中可以發(fā)現(xiàn),native_init和processFile的JNI層函數(shù)被聲明成:

//Java層函數(shù)名中如果有一個”_”的話,轉(zhuǎn)換成JNI后就變成了”_l”。

JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init

JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile

需解釋一下,靜態(tài)方法中native函數(shù)是如何找到對應(yīng)的JNI函數(shù)的。其實,過程非常簡單:

·? 當(dāng)Java層調(diào)用native_init函數(shù)時,它會從對應(yīng)的JNI庫Java_android_media_MediaScanner_native_linit,如果沒有,就會報錯。如果找到,則會為這個native_init和Java_android_media_MediaScanner_native_linit建立一個關(guān)聯(lián)關(guān)系,其實就是保存JNI層函數(shù)的函數(shù)指針。以后再調(diào)用native_init函數(shù)時,直接使用這個函數(shù)指針就可以了,當(dāng)然這項工作是由虛擬機(jī)完成的。

從這里可以看出,靜態(tài)方法就是根據(jù)函數(shù)名來建立Java函數(shù)和JNI函數(shù)之間的關(guān)聯(lián)關(guān)系的,它要求JNI層函數(shù)的名字必須遵循特定的格式。這種方法也有幾個弊端,它們是:

·? 需要編譯所有聲明了native函數(shù)的Java類,每個生成的class文件都得用javah生成一個頭文件。

·? javah生成的JNI層函數(shù)名特別長,書寫起來很不方便。

·? 初次調(diào)用native函數(shù)時要根據(jù)函數(shù)名字搜索對應(yīng)的JNI層函數(shù)來建立關(guān)聯(lián)關(guān)系,這樣會影響運(yùn)行效率。

有什么辦法可以克服上面三種弊端嗎?根據(jù)上面的介紹,Java native函數(shù)是通過函數(shù)指針來和JNI層函數(shù)建立關(guān)聯(lián)關(guān)系的。如果直接讓native函數(shù)知道JNI層對應(yīng)函數(shù)的函數(shù)指針,不就萬事大吉了嗎?這就是下面要介紹的第二種方法:動態(tài)注冊法。

(2)動態(tài)注冊

既然Java native函數(shù)數(shù)和JNI函數(shù)是一一對應(yīng)的,那么是不是會有一個結(jié)構(gòu)來保存這種關(guān)聯(lián)關(guān)系呢?答案是肯定的。在JNI技術(shù)中,用來記錄這種一一對應(yīng)關(guān)系的,是一個叫JNINativeMethod的結(jié)構(gòu),其定義如下:

typedef struct {

?? //Java中native函數(shù)的名字,不用攜帶包的路徑。例如“native_init“。

constchar* name; ???

//Java函數(shù)的簽名信息,用字符串表示,是參數(shù)類型和返回值類型的組合。

?? ?const char* signature;

???void*?????? fnPtr; ?//JNI層對應(yīng)函數(shù)的函數(shù)指針,注意它是void*類型。

} JNINativeMethod;

應(yīng)該如何使用這個結(jié)構(gòu)體呢?來看MediaScanner JNI層是如何做的,代碼如下所示:

[-->android_media_MediaScanner.cpp]

//定義一個JNINativeMethod數(shù)組,其成員就是MS中所有native函數(shù)的一一對應(yīng)關(guān)系。

static JNINativeMethod gMethods[] = {

??? ......

{

"processFile" //Java中native函數(shù)的函數(shù)名。

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

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

?(void*)android_media_MediaScanner_processFile //JNI層對應(yīng)函數(shù)指針。

},

?......

?

{

"native_init",???????

"()V",?????????????????????

(void *)android_media_MediaScanner_native_init

},

? ......

};

//注冊JNINativeMethod數(shù)組

int register_android_media_MediaScanner(JNIEnv*env)

{

?? //調(diào)用AndroidRuntime的registerNativeMethods函數(shù),第二個參數(shù)表明是Java中的哪個類

??? returnAndroidRuntime::registerNativeMethods(env,

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

}

AndroidRunTime類提供了一個registerNativeMethods函數(shù)來完成注冊工作,下面看registerNativeMethods的實現(xiàn),代碼如下:

[-->AndroidRunTime.cpp]

int AndroidRuntime::registerNativeMethods(JNIEnv*env,

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

{

??? //調(diào)用jniRegisterNativeMethods函數(shù)完成注冊

??? returnjniRegisterNativeMethods(env, className, gMethods, numMethods);

}

其中jniRegisterNativeMethods是Android平臺中,為了方便JNI使用而提供的一個幫助函數(shù),其代碼如下所示:

[-->JNIHelp.c]

int jniRegisterNativeMethods(JNIEnv* env, constchar* className,

??? ??????????????????????????????constJNINativeMethod* gMethods, int numMethods)

{

??? jclassclazz;

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

......

//實際上是調(diào)用JNIEnv的RegisterNatives函數(shù)完成注冊的

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

???????return -1;

??? }

??? return0;

}

wow,好像很麻煩?。∑鋵崉討B(tài)注冊的工作,只用兩個函數(shù)就能完成。總結(jié)如下:

/*

env指向一個JNIEnv結(jié)構(gòu)體,它非常重要,后面會討論它。classname為對應(yīng)的Java類名,由于

JNINativeMethod中使用的函數(shù)名并非全路徑名,所以要指明是哪個類。

*/

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

//調(diào)用JNIEnv的RegisterNatives函數(shù),注冊關(guān)聯(lián)關(guān)系。

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

所以,在自己的JNI層代碼中使用這種方法,就可以完成動態(tài)注冊了。這里還有一個很棘手的問題:這些動態(tài)注冊的函數(shù)在什么時候、什么地方被誰調(diào)用呢?好了,不賣關(guān)子了,直接給出該問題的答案:

·? 當(dāng)Java層通過System.loadLibrary加載完JNI動態(tài)庫后,緊接著會查找該庫中一個叫JNI_OnLoad的函數(shù),如果有,就調(diào)用它,而動態(tài)注冊的工作就是在這里完成的。

所以,如果想使用動態(tài)注冊方法,就必須要實現(xiàn)JNI_OnLoad函數(shù),只有在這個函數(shù)中,才有機(jī)會完成動態(tài)注冊的工作。靜態(tài)注冊則沒有這個要求,可我建議讀者也實現(xiàn)這個JNI_OnLoad函數(shù),因為有一些初始化工作是可以在這里做的。

那么,libmedia_jni.so的JNI_OnLoad函數(shù)是在哪里實現(xiàn)的呢?由于多媒體系統(tǒng)很多地方都使用了JNI,所以碼農(nóng)把它放到android_media_MediaPlayer.cpp中了,代碼如下所示:

[-->android_media_MediaPlayer.cpp]

jint JNI_OnLoad(JavaVM* vm, void* reserved)

{

?? //該函數(shù)的第一個參數(shù)類型為JavaVM,這可是虛擬機(jī)在JNI層的代表喔,每個Java進(jìn)程只有一個

? //這樣的JavaVM

???JNIEnv* env = NULL;

??? jintresult = -1;

?

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

?????? ??gotobail;

??? }

?? ?...... //動態(tài)注冊MediaScanner的JNI函數(shù)。

??? if(register_android_media_MediaScanner(env) < 0) {

????????goto bail;

}

......

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

}

JNI函數(shù)注冊的內(nèi)容介紹完了。下面來關(guān)注JNI技術(shù)中其他的幾個重要部分。

JNI層代碼中一般要包含jni.h這個頭文件。Android源碼中提供了一個幫助頭文件JNIHelp.h,它內(nèi)部其實就包含了jni.h,所以我們在自己的代碼中直接包含這個JNIHelp.h即可。

2. 數(shù)據(jù)類型轉(zhuǎn)換

通過前面的分析,解決了JNI函數(shù)的注冊問題。下面來研究數(shù)據(jù)類型轉(zhuǎn)換的問題。

在Java中調(diào)用native函數(shù)傳遞的參數(shù)是Java數(shù)據(jù)類型,那么這些參數(shù)類型到了JNI層會變成什么呢?

Java數(shù)據(jù)類型分為基本數(shù)據(jù)類型和引用數(shù)據(jù)類型兩種,JNI層也是區(qū)別對待這二者的。先來看基本數(shù)據(jù)類型的轉(zhuǎn)換。

(1)基本類型的轉(zhuǎn)換

基本類型的轉(zhuǎn)換很簡單,可用表2-1表示:

表2-1? 基本數(shù)據(jù)類型轉(zhuǎn)換關(guān)系表

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基本數(shù)據(jù)類型和JNI層數(shù)據(jù)類型對應(yīng)的轉(zhuǎn)換關(guān)系,非常簡單。不過,應(yīng)務(wù)必注意,轉(zhuǎn)換成Native類型后對應(yīng)數(shù)據(jù)類型的字長,例如jchar在Native語言中是16位,占兩個字節(jié),這和普通的char占一個字節(jié)的情況完全不一樣。

接下來看Java引用數(shù)據(jù)類型的轉(zhuǎn)換。

(2)引用數(shù)據(jù)類型的轉(zhuǎn)換

引用數(shù)據(jù)類型的轉(zhuǎn)換如表2-2所示:

表2-2? Java引用數(shù)據(jù)類型轉(zhuǎn)換關(guān)系表

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中基本數(shù)據(jù)類型的數(shù)組、Class、String和Throwable外,其余所有Java對象的數(shù)據(jù)類型在JNI中都用jobject表示。

這一點太讓人驚訝了!看processFile這個函數(shù):

//Java層processFile有三個參數(shù)。

processFile(String path, StringmimeType,MediaScannerClient client);

//JNI層對應(yīng)的函數(shù),最后三個參數(shù)和processFile的參數(shù)對應(yīng)。

android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz,

jstring path, jstring mimeType, jobject client)

從上面這段代碼中可以發(fā)現(xiàn):

·? Java的String類型在JNI層對應(yīng)為jstring。

·? Java的MediaScannerClient類型在JNI層對應(yīng)為jobject。

如果對象類型都用jobject表示,就好比是Native層的void*類型一樣,對碼農(nóng)來說,是完全透明的。既然是透明的,那該如何使用和操作它們呢?在回答這個問題之前,再來仔細(xì)看看上面那個android_media_MediaScanner_processFile函數(shù),代碼如下:

/*

Java中的processFile只有三個參數(shù),為什么JNI層對應(yīng)的函數(shù)會有五個參數(shù)呢?第一個參數(shù)中的JNIEnv是什么?稍后介紹。第二個參數(shù)jobject代表Java層的MediaScanner對象,它表示

是在哪個MediaScanner對象上調(diào)用的processFile。如果Java層是static函數(shù)的話,那么

這個參數(shù)將是jclass,表示是在調(diào)用哪個Java Class的靜態(tài)函數(shù)。

*/

android_media_MediaScanner_processFile(JNIEnv*env,

jobject thiz,

jstring path, jstring mimeType, jobject client)

上面的代碼,引出了下面幾節(jié)的主角JNIEnv。

3. JNIEnv介紹

JNIEnv是一個和線程相關(guān)的,代表JNI環(huán)境的結(jié)構(gòu)體,圖2-3展示了JNIEnv的內(nèi)部結(jié)構(gòu):

http://wiki.jikexueyuan.com/project/deep-android-v1/images/chapter2/image003.png" alt="image" />

圖2-3? JNIEnv內(nèi)部結(jié)構(gòu)簡圖

從上圖可知,JNIEnv實際上就是提供了一些JNI系統(tǒng)函數(shù)。通過這些函數(shù)可以做到:

·? 調(diào)用Java的函數(shù)。

·? 操作jobject對象等很多事情。

后面小節(jié)中將具體介紹怎么使用JNIEnv中的函數(shù)。這里,先介紹一個關(guān)于JNIEnv的重要知識點。

上面提到說JNIEnv,是一個和線程有關(guān)的變量。也就是說,線程A有一個JNIEnv,線程B有一個JNIEnv。由于線程相關(guān),所以不能在線程B中使用線程A的JNIEnv結(jié)構(gòu)體。讀者可能會問,JNIEnv不都是native函數(shù)轉(zhuǎn)換成JNI層函數(shù)后由虛擬機(jī)傳進(jìn)來的嗎?使用傳進(jìn)來的這個JNIEnv總不會錯吧?是的,在這種情況下使用當(dāng)然不會出錯。不過當(dāng)后臺線程收到一個網(wǎng)絡(luò)消息,而又需要由Native層函數(shù)主動回調(diào)Java層函數(shù)時,JNIEnv是從何而來呢?根據(jù)前面的介紹可知,我們不能保存另外一個線程的JNIEnv結(jié)構(gòu)體,然后把它放到后臺線程中來用。這該如何是好?

還記得前面介紹的那個JNI_OnLoad函數(shù)嗎?它的第一個參數(shù)是JavaVM,它是虛擬機(jī)在JNI層的代表,代碼如下所示:

//全進(jìn)程只有一個JavaVM對象,所以可以保存,任何地方使用都沒有問題。

jint JNI_OnLoad(JavaVM* vm, void* reserved)

正如上面代碼所說,不論進(jìn)程中有多少個線程,JavaVM卻是獨此一份,所以在任何地方都可以使用它。那么,JavaVM和JNIEnv又有什么關(guān)系呢?答案如下:

·? 調(diào)用JavaVM的AttachCurrentThread函數(shù),就可得到這個線程的JNIEnv結(jié)構(gòu)體。這樣就可以在后臺線程中回調(diào)Java函數(shù)了。

·? 另外,后臺線程退出前,需要調(diào)用JavaVM的DetachCurrentThread函數(shù)來釋放對應(yīng)的資源。

再來看JNIEnv的作用。

4. 通過JNIEnv操作jobject

前面提到過一個問題,即Java的引用類型除了少數(shù)幾個外,最終在JNI層都用jobject來表示對象的數(shù)據(jù)類型,那么該如何操作這個jobject呢?

從另外一個角度來解釋這個問題。一個Java對象是由什么組成的?當(dāng)然是它的成員變量和成員函數(shù)了。那么,操作jobject的本質(zhì)就應(yīng)當(dāng)是操作這些對象的成員變量和成員函數(shù)。所以應(yīng)先來看與成員變量及成員函數(shù)有關(guān)的內(nèi)容。

(1)jfieldID 和jmethodID的介紹

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

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

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

其中,jclass代表Java類,name表示成員函數(shù)或成員變量的名字,sig為這個函數(shù)和變量的簽名信息。如前所示,成員函數(shù)和成員變量都是類的信息,這兩個函數(shù)的第一個參數(shù)都是jclass。

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

[-->android_media_MediaScanner.cpp::MyMediaScannerClient構(gòu)造函數(shù)]

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

{

?//先找到android.media.MediaScannerClient類在JNI層中對應(yīng)的jclass實例。

jclass mediaScannerClientInterface =

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

?//取出MediaScannerClient類中函數(shù)scanFile的jMethodID。

mScanFileMethodID = env->GetMethodID(

mediaScannerClientInterface, "scanFile",

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

?//取出MediaScannerClient類中函數(shù)handleStringTag的jMethodID。

?mHandleStringTagMethodID = env->GetMethodID(

mediaScannerClientInterface,"handleStringTag",

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

? ......

}

在上面代碼中,將scanFile和handleStringTag函數(shù)的jmethodID保存為MyMediaScannerClient的成員變量。為什么這里要把它們保存起來呢?這個問題涉及一個事關(guān)程序運(yùn)行效率的知識點:

·? 如果每次操作jobject前都去查詢jmethoID或jfieldID的話將會影響程序運(yùn)行的效率。所以我們在初始化的時候,就可以取出這些ID并保存起來以供后續(xù)使用。

取出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;

???????

/*

調(diào)用JNIEnv的CallVoidMethod函數(shù),注意CallVoidMethod的參數(shù):

第一個是代表MediaScannerClient的jobject對象,

第二個參數(shù)是函數(shù)scanFile的jmethodID,后面是Java中scanFile的參數(shù)。

*/

???????mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,

lastModified, fileSize);

?

???????mEnv->DeleteLocalRef(pathStr);

???????return (!mEnv->ExceptionCheck());

}

明白了,通過JNIEnv輸出的CallVoidMethod,再把jobject、jMethodID和對應(yīng)參數(shù)傳進(jìn)去,JNI層就能夠調(diào)用Java對象的函數(shù)了!

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

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

其中type是對應(yīng)Java函數(shù)的返回值類型,例如CallIntMethod、CallVoidMethod等。

上面是針對非static函數(shù)的,如果想調(diào)用Java中的static函數(shù),則用JNIEnv輸出的CallStatic<Type>Method系列函數(shù)。

現(xiàn)在,我們已了解了如何通過JNIEnv操作jobject的成員函數(shù),那么怎么通過jfieldID操作jobject的成員變量呢?這里,直接給出整體解決方案,如下所示:

//獲得fieldID后,可調(diào)用Get<type>Field系列函數(shù)獲取jobject對應(yīng)成員變量的值。

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

//或者調(diào)用Set<type>Field系列函數(shù)來設(shè)置jobject對應(yīng)成員變量的值。

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

//下面我們列出一些參加的Get/Set函數(shù)。

GetObjectField()???????? SetObjectField()

GetBooleanField()?? ????? SetBooleanField()

GetByteField()?????????? SetByteField()

GetCharField()?????????? SetCharField()

GetShortField()????????? SetShortField()

GetIntField()??????????? SetIntField()

GetLongField()?????????? SetLongField()

GetFloatField()????????? SetFloatField()

GetDoubleField() ???????????????? SetDoubleField()

通過本節(jié)的介紹,相信讀者已了解jfieldID和jmethodID的作用,也知道如何通過JNIEnv的函數(shù)來操作jobject了。雖然jobject是透明的,但有了JNIEnv的幫助,還是能輕松操作jobject背后的實際對象了。

5. jstring介紹

Java中的String也是引用類型,不過由于它的使用非常頻繁,所以在JNI規(guī)范中單獨創(chuàng)建了一個jstring類型來表示Java中的String類型。雖然jstring是一種獨立的數(shù)據(jù)類型,但是它并沒有提供成員函數(shù)供操作。相比而言,C++中的string類就有自己的成員函數(shù)了。那么該怎么操作jstring呢?還是得依靠JNIEnv提供的幫助。這里看幾個有關(guān)jstring的函數(shù):

·? 調(diào)用JNIEnv的NewString(JNIEnv *env, const jchar*unicodeChars,jsize len),可以從Native的字符串得到一個jstring對象。其實,可以把一個jstring對象看成是Java中String對象在JNI層的代表,也就是說,jstring就是一個Java String。但由于Java String存儲的是Unicode字符串,所以NewString函數(shù)的參數(shù)也必須是Unicode字符串。

·? 調(diào)用JNIEnv的NewStringUTF將根據(jù)Native的一個UTF-8字符串得到一個jstring對象。在實際工作中,這個函數(shù)用得最多。

·? 上面兩個函數(shù)將本地字符串轉(zhuǎn)換成了Java的String對象,JNIEnv還提供了GetStringChars和GetStringUTFChars函數(shù),它們可以將Java String對象轉(zhuǎn)換成本地字符串。其中GetStringChars得到一個Unicode字符串,而GetStringUTFChars得到一個UTF-8字符串。

·? 另外,如果在代碼中調(diào)用了上面幾個函數(shù),在做完相關(guān)工作后,就都需要調(diào)用ReleaseStringChars或ReleaseStringUTFChars函數(shù)對應(yīng)地釋放資源,否則會導(dǎo)致JVM內(nèi)存泄露。這一點和jstring的內(nèi)部實現(xiàn)有關(guān)系,讀者寫代碼時務(wù)必注意這個問題。

為了加深印象,來看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);

......

//調(diào)用JNIEnv的GetStringUTFChars得到本地字符串pathStr

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

......

//使用完后,必須調(diào)用ReleaseStringUTFChars釋放資源

???env->ReleaseStringUTFChars(path, pathStr);

??? ......

}

6. JNI類型簽名的介紹

先來看動態(tài)注冊中的一段代碼:

tatic JNINativeMethod gMethods[] = {

??? ......

{

"processFile"

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

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

?(void*)android_media_MediaScanner_processFile

},

? ......

}

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

根據(jù)前面的介紹可知,它是Java中對應(yīng)函數(shù)的簽名信息,由參數(shù)類型和返回值類型共同組成。不過為什么需要這個簽名信息呢?

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

JNI規(guī)范定義的函數(shù)簽名信息看起來很別扭,不過習(xí)慣就好了。它的格式是:

(參數(shù)1類型標(biāo)示參數(shù)2類型標(biāo)示...參數(shù)n類型標(biāo)示)返回值類型標(biāo)示。

來看processFile的例子:

Java中函數(shù)定義為void processFile(String path, String mimeType)

對應(yīng)的JNI函數(shù)簽名就是

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

?其中,括號內(nèi)是參數(shù)類型的標(biāo)示,最右邊是返回值類型的標(biāo)示,void類型對應(yīng)的標(biāo)示是V。

?當(dāng)參數(shù)的類型是引用類型時,其格式是”L包名;”,其中包名中的”.”換成”/”。上面例子中的

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

函數(shù)簽名不僅看起來麻煩,寫起來更麻煩,稍微寫錯一個標(biāo)點就會導(dǎo)致注冊失敗。所以,在具體編碼時,讀者可以定義字符串宏,這樣改起來也方便。

表2-3是常見的類型標(biāo)示:

表2-3? 類型標(biāo)示示意表

類型標(biāo)示

Java類型

類型標(biāo)示

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

?

?

上面列出了一些常用的類型標(biāo)示。請讀者注意,如果Java類型是數(shù)組,則標(biāo)示中會有一個“[”,另外,引用類型(除基本類型的數(shù)組外)的標(biāo)示最后都有一個“;”。

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

表2-4? 函數(shù)簽名小例子

函數(shù)簽名

Java函數(shù)

“()Ljava/lang/String;”

String f()

“(ILjava/lang/Class;)J”

long f(int i, Class c)

“([B)V”

void f(byte[] bytes)

請讀者結(jié)合表2-3和表2-4左欄的內(nèi)容寫出對應(yīng)的Java函數(shù)。

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

javap –s -p xxx。其中xxx為編譯后的class文件,s表示輸出內(nèi)部數(shù)據(jù)類型的簽名信息,p表示打印所有函數(shù)和成員的簽名信息,而默認(rèn)只會打印public成員和函數(shù)的簽名信息。

有了javap,就不用死記硬背上面的類型標(biāo)示了。

7. 垃圾回收

我們知道,Java中創(chuàng)建的對象最后是由垃圾回收器來回收和釋放內(nèi)存的,可它對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;

}

//假設(shè)在某個時間,有地方調(diào)用callMediaScanner函數(shù)

void callMediaScanner()

{

? //在這個函數(shù)中操作save_thiz,會有問題嗎?

}

上面的做法肯定會有問題,因為和save_thiz對應(yīng)的Java層中的MediaScanner很有可能已經(jīng)被垃圾回收了,也就是說,save_thiz保存的這個jobject可能是一個野指針,如使用它,后果會很嚴(yán)重。

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

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

那該怎么辦?不必?fù)?dān)心,JNI規(guī)范已很好地解決了這一問題,JNI技術(shù)一共提供了三種類型的引用,它們分別是:

·? Local Reference:本地引用。在JNI層函數(shù)中使用的非全局引用對象都是Local Reference。它包括函數(shù)調(diào)用時傳入的jobject、在JNI層函數(shù)中創(chuàng)建的jobject。LocalReference最大的特點就是,一旦JNI層函數(shù)返回,這些jobject就可能被垃圾回收。

·? Global Reference:全局引用,這種對象如不主動釋放,就永遠(yuǎn)不會被垃圾回收。

·? Weak Global Reference:弱全局引用,一種特殊的GlobalReference,在運(yùn)行過程中可能會被垃圾回收。所以在程序中使用它之前,需要調(diào)用JNIEnv的IsSameObject判斷它是不是被回收了。

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

[-->android_media_MediaScanner.cpp::MyMediaScannerClient構(gòu)造函數(shù)]

?MyMediaScannerClient(JNIEnv *env, jobjectclient)

???????:?? mEnv(env),

? ??????//調(diào)用NewGlobalRef創(chuàng)建一個GlobalReference,這樣mClient就不用擔(dān)心被回收了。

???????????mClient(env->NewGlobalRef(client)),

???????????mScanFileMethodID(0),

???????????mHandleStringTagMethodID(0),

???????????mSetMimeTypeMethodID(0)

{

? ......

}

//析構(gòu)函數(shù)

virtual ~MyMediaScannerClient()

{

??mEnv->DeleteGlobalRef(mClient);//調(diào)用DeleteGlobalRef釋放這個全局引用。

?}

每當(dāng)JNI層想要保存Java層中的某個對象時,就可以使用Global Reference,使用完后記住釋放它就可以了。這一點很容易理解。下面要講有關(guān)LocalReference的一個問題,還是先看實例,代碼如下所示:

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

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

long long fileSize)

{

?? jstringpathStr;

?? //調(diào)用NewStringUTF創(chuàng)建一個jstring對象,它是Local Reference類型。

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

??????? //調(diào)用Java的scanFile函數(shù),把這個jstring傳進(jìn)去

???????mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,

lastModified, fileSize);

???? /*

???? ?根據(jù)LocalReference的說明,這個函數(shù)返回后,pathStr對象就會被回收。所以

????? 下面這個DeleteLocalRef調(diào)用看起來是多余的,其實不然,這里解釋一下原因:

1)如果不調(diào)用DeleteLocalRef,pathStr將在函數(shù)返回后被回收。

2)如果調(diào)用DeleteLocalRef的話,pathStr會立即被回收。這兩者看起來沒什么區(qū)別,

不過代碼要是像下面這樣的話,虛擬機(jī)的內(nèi)存就會被很快被耗盡:

????? for(i