編輯:關於Android編程
./frameworks/base/media/java/android/media/MediaScanner.java ./frameworks/base/media/jni/android_media_MediaScanner.cpp ./frameworks/base/media/jni/android_media_MediaPlayer.cpp ./frameworks/base/media/jni/AndroidRuntime.cpp ./libnativehelper/JNIHelp.cpp
// frameworks/base/media/java/android/media/MediaScanner.java public class MediaScanner { static {//class被加載的時候自動掉用static裡邊的函數,,java的基礎,, /*加載對應的JNI庫*/ System.loadLibrary("media_jni"); //這裡負責加載JNI模塊編譯出來的庫。在實際加載動態庫時會將其拓展為libmedia_jni.so。 native_init(); //調用native_init()函數,這個函數是對應的cpp文件裡邊的函數 } ........ //聲明一個native函數,native為java關鍵字,表示它將由JNI層完成 private static native final void native_init(); private native final void native_setup(); ........ }
注:native_init函數位於android.media這個包中,其全路徑名稱為android.media.MediaScanner.nantive_init。
根據規則其對應的JNI層函數名稱為:android_media_MediaScanner_native_init。
//frameworks/base/media/jni/android_media_MediaScanner.cpp //以下是frameworks/base/media/jni/Android.mk的編譯腳本 /* LOCAL_SRC_FILES:= \ ...\ android_media_MediaScanner.cpp \ ...\ LOCAL_MODULE:= libmedia_jni //編譯生成庫的名字為libmedia_jni.so include $(BUILD_SHARED_LIBRARY) */ static const char* const kClassMediaScanner = //MediaScanner.java的路徑!! "android/media/MediaScanner"; ........ /*native_init函數的JNI層實現*/ static void android_media_MediaScanner_native_init(JNIEnv *env) { ALOGV("native_init"); //FindClass根據路徑尋找java class! jclass clazz = env->FindClass(kClassMediaScanner); if (clazz == NULL) { return; } fields.context = env->GetFieldID(clazz, "mNativeContext", "I"); if (fields.context == NULL) { return; } }
大體的流程如下:
1. 先編譯java代碼,然後編譯生成.class文件
2. 使用java的工具程序javah,如javah -o output packagename.classname,這樣它會生成一個叫output.h的JNI層頭文件。這裡packagename.classname就是上面編譯生成的class文件,而在這裡生成的output.h文件裡則聲明了對應的JNI層函數,只要實現裡邊的函數即可。
靜態注冊中,java函數是怎麼找到對應的jni函數的?其實就是用名字找到的。比如在java中調用native_init函數的時候,它就會在JNI庫中尋找android_media_MediaScanner_native_init函數,如果沒有就會報錯。如果找到,則會為這兩個函數建立連接,其實就是保存JNI層函數的函數指針。以後再調用native_init函數時,直接使用這個函數指針就可以了,當然這項工作是虛擬機完成的。
上面說過java native函數和JNI函數是一一對應的,所以動態注冊方式就采用JNINativeMethod的結構體來記錄這種關系。JNINativeMethod都是定義在各自的JNI層文件中。
typedef struct { const char* name; //保存JNI對應的java函數的名字,比如"native_init",不用加路徑 const char* signature;//保存java函數的簽名信息,用字符串表示,是參數類型和返回值類型的組合 void* fnPtr;//JNI層對應函數的函數指針,注意它是void *類型 } JNINativeMethod; /*比如android_media_MediaScanner.cpp文件中,定義了如下一個JNINativeMethod數組*/ static JNINativeMethod gMethods[] = { { "processDirectory",//java中native函數的函數名 //processFile的簽名信息 "(Ljava/lang/String;Landroid/media/MediaScannerClient;)V", (void *)android_media_MediaScanner_processDirectory//JNI層對應的函數指針 }, ....... { "native_init",//java中native函數的函數名 "()V",//native_init函數的簽名信息 (void *)android_media_MediaScanner_native_init /*JNI層native_init函數的函數指針*/ }, { "native_setup", "()V", (void *)android_media_MediaScanner_native_setup }, ....... }; /*注冊JNINativeMethod數組*/ int register_android_media_MediaScanner(JNIEnv *env) { return AndroidRuntime::registerNativeMethods(env, kClassMediaScanner, gMethods, NELEM(gMethods)); }
接著調用AndroidRuntime中的registerNativeMethods:
//AndroidRuntime.cpp /*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { return jniRegisterNativeMethods(env, className, gMethods, numMethods); } //JNIHelp.cpp extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { JNIEnv* e = reinterpret_cast(env); ALOGV("Registering %s natives", className); scoped_local_ref c(env, findClass(env, className)); if (c.get() == NULL) { ALOGE("Native registration unable to find class '%s', aborting", className); abort(); } if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) { ALOGE("RegisterNatives failed for '%s', aborting", className); abort(); } return 0; }
上面這些說了JNI層怎麼定義的注冊函數等等,那上面的register_android_media_MediaScanner()這種注冊函數是在什麼時間被調用來完成注冊的呢??
當Java層通過System.loadLibrary加載完JNI動態庫後,緊接著會查找一個JNI_OnLoad的函數。如果有就調用它,動態注冊的工作在這裡完成。
//android_media_MediaPlayer.cpp jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env = NULL; jint result = -1; if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { ALOGE("ERROR: GetEnv failed\n"); goto bail; } assert(env != NULL); ...... //下面可以看到有調用動態注冊函數!! if (register_android_media_MediaScanner(env) < 0) { ALOGE("ERROR: MediaScanner native registration failed\n"); goto bail; } ....... /* success -- return valid version number */ result = JNI_VERSION_1_4; bail: return result; }
java層的數據類型和Jni層的數據類型的轉換關系
Java的基本類型和Native層的基本類型轉換非常簡單,不過必須注意轉換成Native類型後對應數據類型的字長,例如jchar在Native語言中是16位,占兩個字節,這和普通的char占一個自己的情況是不一樣的。
下面是Java引用數據類型和Native類型的轉換表
JNIEnv用來操作java類的對象,比如讀取修改java類的類成員變量,或者直接調用java類的成員函數。
JNIEnv變量,在每個JNI層函數中都是以第一個參數傳入。JNIEnv變量可以看到在JNI_Onload()函數中,由Java VM相關函數GetEnv函數取出
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */){ JNIEnv* env = NULL; ... if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { ... } ... }
jobject即java對象在JNI中的表示,通過JNIEnv可以操作jobject以達到讀取修改java類的成員變量和調用java函數的目的。
jfieldID和jmethodID介紹
jfieldID和jmethodID分別代表jobject中成員變量以及成員函數,可以從JNIEnv對應的函數中得到jfieldID
和jmethodID
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
獲取jmethodID和使用方法
class MyMediaScannerClient : public MediaScannerClient { public: MyMediaScannerClient(JNIEnv *env, jobject client) //構造函數中設置mClient等 : mEnv(env), mClient(env->NewGlobalRef(client)), mScanFileMethodID(0), mHandleStringTagMethodID(0), mSetMimeTypeMethodID(0) { ... mSetMimeTypeMethodID = env->GetMethodID( mediaScannerClientInterface, "setMimeType", "(Ljava/lang/String;)V"); ... } }
virtual status_t setMimeType(const char* mimeType) { jstring mimeTypeStr; ... //mClient就是構造函數中獲取的jobject //mSetMimeTypeMethodID就是構造函數中調用GetMethodID獲取到的響應的jmethondID mEnv->CallVoidMethod(mClient, mSetMimeTypeMethodID, mimeTypeStr); ... }
獲取jfieldID和使用的例子
static void android_media_MediaScanner_native_init(JNIEnv *env) { ... jclass clazz = env->FindClass(kClassMediaScanner); fields.context = env->GetFieldID(clazz, "mNativeContext", "J"); .... }
使用下面的函數修改或者讀取jfieldID對應的成員變量,這裡注意變量的類型!!!
//修改對應的變量 static void setNativeScanner_l(JNIEnv* env, jobject thiz, MediaScanner *s) { env->SetLongField(thiz, fields.context, (jlong)s); } //讀取對應的變量 static MediaScanner *getNativeScanner_l(JNIEnv* env, jobject thiz) { return (MediaScanner *) env->GetLongField(thiz, fields.context); }
Java中創建的對象最後是由垃圾回收器來回收和釋放內存的,可它對JNI有什麼影響呢?下面看一個例子
static jobject save_thiz = NULL;//定義一個全局的jobject static void android_media_MediaScanner_processFile( JNIEnv *env, jobject thiz, jstring path, jstring mimeType, 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的引用計數Local Reference:本地引用。在JNI層函數中使用的非全局引用對象都是Local Reference,它包含函數調用時傳入的jobject和在JNI層函數中創建的jobject。Local Reference最大的特點就是,一旦JNI層函數返回,這些jobject就可能被垃圾回收 Global Reference:全局引用,這種對象如果不主動釋放,它永遠不會被垃圾回收
Weak Global Reference:弱全局引用,一種特殊的Global Reference,在運行過程中可能會被垃圾回收。所以在使用它之前,需要調用JNIEnv的IsSameObject判斷它是否被回收了
平時用得最多的是Local Reference和Global Reference,下面來看一個例子,代碼如下:
“`c
public:
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
//調用NewGlobalRef創建一個Global Reference,這樣mClient就不用擔心被回收了
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
…
}
//析構函數
virtual ~MyMediaScannerClient()
{
mEnv->DeleteGlobalRef(mClient); //DeleteGlobalRef函數釋放這個全局引用
}
像上面這樣,每當JNI層想要保存Java層中的某個對象時,就可以使用Global Reference,使用完後記住釋放它就可以了。 下面來看一下Local Reference。 ```c virtual status_t scanFile(const char* path, long long lastModified, long long fileSize, bool isDirectory, bool noMedia) { jstring pathStr; //調用NewStringUTF創建一個jstring對象,它是Local Reference類型 if ((pathStr = mEnv->NewStringUTF(path)) == NULL) { mEnv->ExceptionClear(); return NO_MEMORY; } mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize, isDirectory, noMedia); //調用DeleteLocalRef釋放Local Reference,按照前面的說法,在return之後, //pathStr就會被釋放,所以這裡釋放Local Reference好像是多余的,但有區別 //1)如果不調用DeleteLocalRef,pathStr將在函數返回後被釋放 //2)調用DeleteLocalRef,pathStr將立即被釋放 //由於垃圾回收時間不定而且如果在頻繁調用NewStringUTF的時候, //還是需要馬上釋放Local Reference,像下面這樣不馬上釋放的話內存會馬上被耗光 for(int i=0;i<100;i++){ jstring pathStr = mEnv->NewStringUTF(path); //mEnv->DeleteLocalRef(pathStr); } mEnv->DeleteLocalRef(pathStr); return checkAndClearExceptionFromCallback(mEnv, "scanFile"); }
JNI中也有一場,如果調用JNIEnv的某些函數出錯了,則會產生一個異常,但這個異常不會中斷本地函數的執行,直到從JNI層返回到Java層後,虛擬機才會拋出這個異常。雖然在JNI層中產生的異常不會中斷本地函數的運行,但一旦產生異常後,就只能做一些資源清理工作了(例如釋放全局引用,或者ReleaseStringChars)。如果這時調用除上面提到的函數之外的其他JNIEnv函數,則會導致程序死掉。
來看一個和異常處理有關的例子,代碼如下所示:
virtual status_t scanFile(const char* path, long long lastModified, long long fileSize, bool isDirectory, bool noMedia) { jstring pathStr; if ((pathStr = mEnv->NewStringUTF(path)) == NULL) { mEnv->ExceptionClear();//清理當前JNI層中發生的異常 return NO_MEMORY; } }
JNI層函數可以在代碼中截獲和修改這些異常,JNIEnv提供了三個函數給予幫助:
ExceptionOccured函數,用來判斷是否發生異常 ExceptionClear函數,用來清理當前JNI層中發生的異常 ThrowNew函數,用來向Java層拋出異常定義外觀模式(Facade Pattern):外部與一個子系統的通信必須通過一個統一的外觀對象進行,為子系統中的一組接口提供一個一致的界面,外觀模式定義了一個高層接口,這
android系統中的每個ViewGroup的子類都具有下面三個和TouchEvent處理密切相關的方法: 1)public boolean dispatchTouc
為了解決65535方法數超標的問題,Google推薦使用MultiDex來加載classes2.dex,classes3.dex等等,其基本思想就是在運行時動態修改Cla
本文要演示的Android開發實例是如何完成一個Android中的miniTwitter登錄界面,下面將分步驟講解怎樣實現圖中的界面效果,讓大家都能輕松的做出美觀的登錄界