編輯:Android資訊
JNI技術對於多java開發的朋友相信並不陌生,即(java native interface),本地調用接口,主要功能有以下兩點:
1、java層調用C/C++層代碼
2、C/C++層調用java層代碼
可能有些人會覺得jni技術破壞了Java語言的跨平台性,有這種想法可能是因為你對java理解得還不夠深,如果你看看jdk源碼,你會發現在jdk裡面大量使用了jni技術,而且java虛擬機就是用本地語言寫的,所以導致jvm並不能跨平台性,所以說java的跨平台性並不是100%的跨平台的。相反你應該看到使用Jni的優勢:
1、因為C/C++語言本來機比java語言誕生早,所以很多庫代碼都是使用C/C++寫的,有了Jni我們就可以直接使用了,不用重復造輪子。
2、不可否認,C/C++執行效率比java 高,對於一些對效率有要求的功能,必須使用C/C++.
由於打算研究Android 中java層和native層是如何連接起來的,所以想研究一下Android中的jni技術(在閱讀之前,最好了解jni中的基本知識,如jni中數據類型,簽名格式,不然看起來可能有些吃力),由於工作和MediaPlayer有關,這裡就使用MediaPlayer為例吧。
下面給出一張圖,通過此圖,我們簡要說明一下jni是如何連接Java層和本地層的。
當我們的app要播放視頻的時候,我們使用的是java層的MediaPlayer類,我們進入到MediaPlayer.java看看(提醒:我這裡使用的是源碼4.1)
主要注意的有兩點:
1、靜態代碼塊:
static { System.loadLibrary("media_jni"); native_init(); }
2、native_init的簽名:
private static native final void native_init();
看到靜態代碼塊後,我們可以知道MediaPlayer對應的jni層代碼在Media_jni.so庫中
本地層對應的so庫是libmedia.so,所以MediaPlayer.java通過Media_jni.so和MediaPlayer.cpp(libmedia.so)進行交互
下面我們就深入到細節吧。不過在深入細節前,我先要告訴你一個規則,在Android中,通常java層類和jni層類的名字有如下關系,拿MediaPlayer為例,java層叫android.media.MediaPlayer.java,那麼jni層叫做android_media_MediaPlayer.cpp
由於native_init是一個本地方法,那麼我們就到android_media_MediaPlayer.cpp找到native_init的對應方法吧
static void android_media_MediaPlayer_native_init(JNIEnv *env) { jclass clazz; clazz = env->FindClass("android/media/MediaPlayer"); if (clazz == NULL) { return; } fields.context = env->GetFieldID(clazz, "mNativeContext", "I"); if (fields.context == NULL) { return; } fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative", "(Ljava/lang/Object;IIILjava/lang/Object;)V"); if (fields.post_event == NULL) { return; } fields.surface_texture = env->GetFieldID(clazz, "mNativeSurfaceTexture", "I"); if (fields.surface_texture == NULL) { return; } }
對應上面的代碼,如果你對java中的反射理解得很透徹的話,其實很好理解,首先找到java層的MediaPlayer的Class對象,jclass是java層Class在native層的代碼,然後分別保存mNaviceContext字段,postEventFromNative方法,mNativeSurfaceTexture字段。
其實這裡我最想說明的是另外一個問題,就是MediaPlayer中的native_init方法時如何跟android_media_MediaPlayer.cpp中的android_media_MediaPlayer_native_init對應起來的,因為我們知道如果使用javah自動生成的頭文件,那麼在jni層的名字應該是java_android_media_MediaPlayer_native_linit。其實這裡涉及到一個動態注冊的過程。
其實在java層代用System.loadLibrary成功後,就會調用jni文件中的JNI_onLoad方法,android_media_MediaPlayer.cpp中的JNI_onLoad方法如下(截取部分)
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_MediaPlayer(env) < 0) { ALOGE("ERROR: MediaPlayer native registration failed\n"); goto bail; } /* success -- return valid version number */ result = JNI_VERSION_1_4; bail: return result; }
這裡有一個方法叫做register_android_media_MediaPlayer,我們進入此方法,看看注冊了什麼
static int register_android_media_MediaPlayer(JNIEnv *env) { return AndroidRuntime::registerNativeMethods(env, "android/media/MediaPlayer", gMethods, NELEM(gMethods)); }
這裡就是調用了AndroidRuntime提供的registerNativeMethods方法,這裡涉及到一個gMethods的變量,它其實是一個結構體
typedef struct { const char* name; const char* signature; void* fnPtr; } JNINativeMethod;
name:就是在java層方法名稱
signature:就是方法在簽名
fnPtr:在jni層對應的函數名稱
,那麼我們找到native_init在gMethods對應的值吧
static JNINativeMethod gMethods[] = { { "_setDataSource", "(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)V", (void *)android_media_MediaPlayer_setDataSourceAndHeaders }, .... {"native_init", "()V", (void *)android_media_MediaPlayer_native_init}, ... };
接下來,我們看看AndroidRuntime中的registerNativeMethods做了什麼吧
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { return jniRegisterNativeMethods(env, className, gMethods, numMethods); }
調用了jniRegisterNativeMethods
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { JNIEnv* e = reinterpret_cast<JNIEnv*>(env); ALOGV("Registering %s natives", className); scoped_local_ref<jclass> 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; }
最終調用了env的RegisterNativers完成了注冊。
其實寫到這裡,我們已經知道了java層和jni是如何聯系起來的,接下來我想說的是jni是如何將java層和native聯系起來的,還是用MediaPlayer為例吧,我們進入MediaPlayer的構造函數。
public MediaPlayer() { Looper looper; if ((looper = Looper.myLooper()) != null) { mEventHandler = new EventHandler(this, looper); } else if ((looper = Looper.getMainLooper()) != null) { mEventHandler = new EventHandler(this, looper); } else { mEventHandler = null; } /* Native setup requires a weak reference to our object. * It's easier to create it here than in C++. */ native_setup(new WeakReference<MediaPlayer>(this)); }
這裡創建了一個mEventHandler對象,並調用了native_setup方法,我們進入到android_media_MediaPlayer.cpp的對應方法看看
static void android_media_MediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this) { ALOGV("native_setup"); sp<MediaPlayer> mp = new MediaPlayer(); if (mp == NULL) { jniThrowException(env, "java/lang/RuntimeException", "Out of memory"); return; } // create new listener and give it to MediaPlayer sp<JNIMediaPlayerListener> listener = new JNIMediaPlayerListener(env, thiz, weak_this); mp->setListener(listener); // Stow our new C++ MediaPlayer in an opaque field in the Java object. setMediaPlayer(env, thiz, mp); }
這裡創建了一個本地MediaPlayer對象,並且設置了listener,(如果做過播放器的同學應該知道這個listener應該知道干啥,不知道也沒關系),最後調用了setMediaPlayer方法,這個才是我們需要關注的。
static sp<MediaPlayer> setMediaPlayer(JNIEnv* env, jobject thiz, const sp<MediaPlayer>& player) { Mutex::Autolock l(sLock); sp<MediaPlayer> old = (MediaPlayer*)env->GetIntField(thiz, fields.context); if (player.get()) { player->incStrong(thiz); } if (old != 0) { old->decStrong(thiz); } env->SetIntField(thiz, fields.context, (int)player.get()); return old; }
其實就是先拿到fields.context的對應的值,還記得這個這個值是什麼嗎,不記得的可以回到上面看看
fields.context = env->GetFieldID(clazz, "mNativeContext", "I");
其實就是java層mNativeContext對應的值,就是將本地MediaPlayer的地址存放到mNativeContext中。
現在加入我們要播放一個本地Mp4視頻,那麼使用如下代碼即可
mediaPlayer.setDataSource("/mnt/sdcard/a.mp4"); mediaPlayer.setDisplay(surface1.getHolder()); mediaPlayer.prepare(); mediaPlayer.start();
其實這裡調用的 幾個都是本地方法,這裡我就是用prepare方法為例,講解MediaPlaeyr.java和MediaPlayer.cpp的交互
當在java層調用prepare方法時,在jni層會調用如下方法
static void android_media_MediaPlayer_prepare(JNIEnv *env, jobject thiz) { sp<MediaPlayer> mp = getMediaPlayer(env, thiz); if (mp == NULL ) { jniThrowException(env, "java/lang/IllegalStateException", NULL); return; } // Handle the case where the display surface was set before the mp was // initialized. We try again to make it stick. sp<ISurfaceTexture> st = getVideoSurfaceTexture(env, thiz); mp->setVideoSurfaceTexture(st); process_media_player_call( env, thiz, mp->prepare(), "java/io/IOException", "Prepare failed." ); }
這裡通過getMediaPlayer方法拿到本地的MediaPlayer對象,調用調用本地方法process_media_player_call,並將本地MediaPlayer調用parepare方法的結果傳遞給此方法。
static void process_media_player_call(JNIEnv *env, jobject thiz, status_t opStatus, const char* exception, const char *message) { if (exception == NULL) { // Don't throw exception. Instead, send an event. if (opStatus != (status_t) OK) { sp<MediaPlayer> mp = getMediaPlayer(env, thiz); if (mp != 0) mp->notify(MEDIA_ERROR, opStatus, 0); } } else { // Throw exception! if ( opStatus == (status_t) INVALID_OPERATION ) { jniThrowException(env, "java/lang/IllegalStateException", NULL); } else if ( opStatus == (status_t) PERMISSION_DENIED ) { jniThrowException(env, "java/lang/SecurityException", NULL); } else if ( opStatus != (status_t) OK ) { if (strlen(message) > 230) { // if the message is too long, don't bother displaying the status code jniThrowException( env, exception, message); } else { char msg[256]; // append the status code to the message sprintf(msg, "%s: status=0x%X", message, opStatus); jniThrowException( env, exception, msg); } } } }
在這個裡面根據prepare返回的狀態,如果exception==null 並且prepare執行失敗,測試不拋異常,而是調用本地MediaPlayer的notify方法。
void MediaPlayer::notify(int msg, int ext1, int ext2, const Parcel *obj) { ALOGV("message received msg=%d, ext1=%d, ext2=%d", msg, ext1, ext2); bool send = true; bool locked = false; ... switch (msg) { case MEDIA_NOP: // interface test message break; case MEDIA_PREPARED: ALOGV("prepared"); mCurrentState = MEDIA_PLAYER_PREPARED; if (mPrepareSync) { ALOGV("signal application thread"); mPrepareSync = false; mPrepareStatus = NO_ERROR; mSignal.signal(); } break; case MEDIA_PLAYBACK_COMPLETE: ALOGV("playback complete"); if (mCurrentState == MEDIA_PLAYER_IDLE) { ALOGE("playback complete in idle state"); } if (!mLoop) { mCurrentState = MEDIA_PLAYER_PLAYBACK_COMPLETE; } break; case MEDIA_ERROR: // Always log errors. // ext1: Media framework error code. // ext2: Implementation dependant error code. ALOGE("error (%d, %d)", ext1, ext2); mCurrentState = MEDIA_PLAYER_STATE_ERROR; if (mPrepareSync) { ALOGV("signal application thread"); mPrepareSync = false; mPrepareStatus = ext1; mSignal.signal(); send = false; } break; case MEDIA_INFO: // ext1: Media framework error code. // ext2: Implementation dependant error code. if (ext1 != MEDIA_INFO_VIDEO_TRACK_LAGGING) { ALOGW("info/warning (%d, %d)", ext1, ext2); } break; case MEDIA_SEEK_COMPLETE: ALOGV("Received seek complete"); if (mSeekPosition != mCurrentPosition) { ALOGV("Executing queued seekTo(%d)", mSeekPosition); mSeekPosition = -1; seekTo_l(mCurrentPosition); } else { ALOGV("All seeks complete - return to regularly scheduled program"); mCurrentPosition = mSeekPosition = -1; } break; case MEDIA_BUFFERING_UPDATE: ALOGV("buffering %d", ext1); break; case MEDIA_SET_VIDEO_SIZE: ALOGV("New video size %d x %d", ext1, ext2); mVideoWidth = ext1; mVideoHeight = ext2; break; case MEDIA_TIMED_TEXT: ALOGV("Received timed text message"); break; default: ALOGV("unrecognized message: (%d, %d, %d)", msg, ext1, ext2); break; } sp<MediaPlayerListener> listener = mListener; if (locked) mLock.unlock(); // this prevents re-entrant calls into client code if ((listener != 0) && send) { Mutex::Autolock _l(mNotifyLock); ALOGV("callback application"); listener->notify(msg, ext1, ext2, obj); ALOGV("back from callback"); } }
做過播放器的同學應該對上面幾個消息都不陌生吧,由於剛才調用prepare方法失敗了,所以這裡應該執行MEDIA_ERROR分支,最後調用listener的notify代碼,這個listener就是在native_setup中設置的
void JNIMediaPlayerListener::notify(int msg, int ext1, int ext2, const Parcel *obj) { JNIEnv *env = AndroidRuntime::getJNIEnv(); if (obj && obj->dataSize() > 0) { jobject jParcel = createJavaParcelObject(env); if (jParcel != NULL) { Parcel* nativeParcel = parcelForJavaObject(env, jParcel); nativeParcel->setData(obj->data(), obj->dataSize()); env->CallStaticVoidMethod(mClass, fields.post_event, mObject, msg, ext1, ext2, jParcel); } } else { env->CallStaticVoidMethod(mClass, fields.post_event, mObject, msg, ext1, ext2, NULL); } if (env->ExceptionCheck()) { ALOGW("An exception occurred while notifying an event."); LOGW_EX(env); env->ExceptionClear(); } }
還記得fields.post_event保存的是什麼嗎
fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative", "(Ljava/lang/Object;IIILjava/lang/Object;)V");
就是java層MediaPlayer的postEventFromNative方法,也就是說如果播放出錯了,那麼就通過調用postEventFromNative方法來告訴java層的MediaPlayer。
private static void postEventFromNative(Object mediaplayer_ref, int what, int arg1, int arg2, Object obj) { MediaPlayer mp = (MediaPlayer)((WeakReference)mediaplayer_ref).get(); if (mp == null) { return; } if (what == MEDIA_INFO && arg1 == MEDIA_INFO_STARTED_AS_NEXT) { // this acquires the wakelock if needed, and sets the client side state mp.start(); } if (mp.mEventHandler != null) { Message m = mp.mEventHandler.obtainMessage(what, arg1, arg2, obj); mp.mEventHandler.sendMessage(m); } }
這個時間最終通過mEventHandler處理,也就是在我們app進程中處理這個錯誤。
寫到這裡,相信你應該對java層和native層的交互有了導致的了解。
什麼是Context? 一個Context意味著一個場景,一個場景就是我們和軟件進行交互的一個過程。比如當你使用微信的時候,場景包括聊天界面、通訊錄、朋友圈,以及
在過去的幾天裡,我有了開發生涯中最有意義的經歷之一, 想在這裡跟大家分享。 現在我們已經讓 ClojureScript 可以在 Android 上運行了。不是在一
繼前面寫的兩篇文章之後( 有問題歡迎反饋哦 ): Android開發:Translucent System Bar 的最佳實踐 Android開發:最詳細的 T
1 背景 其實有點不想寫這篇文章的,但是又想寫,有些矛盾。不想寫的原因是隨便上網一搜一堆關於性能的建議,感覺大家你一總結、我一總結的都說到了很多優化注意事項,但是