Android JNI原理分析
引言:分析Android源碼6.0的過程,一定離不開Java與C/C++代碼直接的來回跳轉,那么就很有必要掌握JNI,這是鏈接Java層和Native層的橋梁,本文涉及相關源碼:
frameworks/base/core/jni/AndroidRuntime.cpplibcore/luni/src/main/java/java/lang/System.java libcore/luni/src/main/java/java/lang/Runtime.java libnativehelper/JNIHelp.cpp libnativehelper/include/nativehelper/jni.hframeworks/base/core/java/android/os/MessageQueue.java frameworks/base/core/jni/android_os_MessageQueue.cppframeworks/base/core/java/android/os/Binder.java frameworks/base/core/jni/android_util_Binder.cppframeworks/base/media/java/android/media/MediaPlayer.java frameworks/base/media/jni/android_media_MediaPlayer.cpp一、JNI概述
JNI(Java Native Interface,Java本地接口),用于打通Java層與Native(C/C++)層。這不是Android系統所獨有的,而是Java所有。眾所周知,Java語言是跨平臺的語言,而這跨平臺的背后都是依靠Java虛擬機,虛擬機采用C/C++編寫,適配各個系統,通過JNI為上層Java提供各種服務,保證跨平臺性。
相信不少經常使用Java的程序員,享受著其跨平臺性,可能全然不知JNI的存在。在Android平臺,讓JNI大放異彩,為更多的程序員所熟知,往往為了提供效率或者其他功能需求,就需要NDK開發。上一篇文章Linux系統調用(syscall)原理,介紹了打通android上層與底層kernel的樞紐syscall,那么本文的目的則是介紹打通android上層中Java層與Native的紐帶JNI。
二、JNI查找方式
Android系統在啟動啟動過程中,先啟動Kernel創建init進程,緊接著由init進程fork第一個橫穿Java和C/C++的進程,即Zygote進程。Zygote啟動過程中會AndroidRuntime.cpp中的startVm創建虛擬機,VM創建完成后,緊接著調用startReg完成虛擬機中的JNI方法注冊。
2.1 startReg
[–>AndroidRuntime.cpp]
int AndroidRuntime::startReg(JNIEnv* env) {//設置線程創建方法為javaCreateThreadEtcandroidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);env->PushLocalFrame(200);//進程NI方法的注冊if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {env->PopLocalFrame(NULL);return -1;}env->PopLocalFrame(NULL);return 0; }register_jni_procs(gRegJNI, NELEM(gRegJNI), env)這行代碼的作用就是就是循環調用gRegJNI數組成員所對應的方法。
static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env) {for (size_t i = 0; i < count; i++) {if (array[i].mProc(env) < 0) {return -1;}}return 0; }gRegJNI數組,有100多個成員變量,定義在AndroidRuntime.cpp:
static const RegJNIRec gRegJNI[] = {REG_JNI(register_android_os_MessageQueue),REG_JNI(register_android_os_Binder),... };該數組的每個成員都代表一個類文件的jni映射,其中REG_JNI是一個宏定義,在Zygote中介紹過,該宏的作用就是調用相應的方法。
2.2 如何查找native方法
當大家在看framework層代碼時,經常會看到native方法,這是往往需要查看所對應的C++方法在哪個文件,對應哪個方法?下面從一個實例出發帶大家如何查看java層方法所對應的native方法位置。
2.2.1 實例(一)
當分析Android消息機制源碼,遇到MessageQueue.java中有多個native方法,比如:
private native void nativePollOnce(long ptr, int timeoutMillis); 步驟1:
MessageQueue.java的全限定名為android.os.MessageQueue.java,方法名:android.os.MessageQueue.nativePollOnce(),而相對應的native層方法名只是將點號替換為下劃線,可得android_os_MessageQueue_nativePollOnce()。
Tips:?nativePollOnce ==> android_os_MessageQueue_nativePollOnce()
步驟2:
有了native方法,那么接下來需要知道該native方法所在那個文件。前面已經介紹過Android系統啟動時就已經注冊了大量的JNI方法,見AndroidRuntime.cpp的gRegJNI數組。這些注冊方法命令方式:
那么MessageQueue.java所定義的jni注冊方法名應該是register_android_os_MessageQueue,的確存在于gRegJNI數組,說明這次JNI注冊過程是有開機過程完成的。 該方法在AndroidRuntime.cpp申明為extern方法:
extern int register_android_os_MessageQueue(JNIEnv* env);這些extern方法絕大多數位于/framework/base/core/jni/目錄,大多數情況下native文件命名方式:
[包名]_[類名].cpp [包名]_[類名].hTips:?MessageQueue.java ==> android_os_MessageQueue.cpp
打開android_os_MessageQueue.cpp文件,搜索android_os_MessageQueue_nativePollOnce方法,這便找到了目標方法:
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,jlong ptr, jint timeoutMillis) {NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);nativeMessageQueue->pollOnce(env, obj, timeoutMillis); }到這里完成了一次從Java層方法搜索到所對應的C++方法的過程。
2.2.2 實例(二)
對于native文件命名方式,有時并非[包名]_[類名].cpp,比如Binder.java
Binder.java所對應的native文件:android_util_Binder.cpp
public static final native int getCallingPid();根據實例(一)方式,找到getCallingPid ==> android_os_Binder_getCallingPid(),并且在AndroidRuntime.cpp中的gRegJNI數組中找到register_android_os_Binder。
按實例(一)方式則native文名應該為android_os_Binder.cpp,可是在/framework/base/core/jni/目錄下找不到該文件,這是例外的情況。其實真正的文件名為android_util_Binder.cpp,這就是例外,這一點有些費勁,不明白為何google要如此打破規律的命名。
static jint android_os_Binder_getCallingPid(JNIEnv* env, jobject clazz) {return IPCThreadState::self()->getCallingPid(); }有人可能好奇,既然如何遇到打破常規的文件命令,怎么辦?這個并不難,首先,可以嘗試在/framework/base/core/jni/中搜索,對于binder.java,可以直接搜索binder關鍵字,其他也類似。如果這里也找不到,可以通過grep全局搜索android_os_Binder_getCallingPid這個方法在哪個文件。
2.2.3 實例(三)
前面兩種都是在Android系統啟動之初,便已經注冊過JNI所對應的方法。 那么如果程序自己定義的jni方法,該如何查看jni方法所在位置呢?下面以MediaPlayer.java為例,其包名為android.media:
public class MediaPlayer{static {System.loadLibrary("media_jni");native_init();}private static native final void native_init();... }通過static靜態代碼塊中System.loadLibrary方法來加載動態庫,庫名為media_jni, Android平臺則會自動擴展成所對應的libmedia_jni.so庫。 接著通過關鍵字native加在native_init方法之前,便可以在java層直接使用native層方法。
接下來便要查看libmedia_jni.so庫定義所在文件,一般都是通過Android.mk文件定義LOCAL_MODULE:= libmedia_jni,可以采用grep或者mgrep來搜索包含libmedia_jni字段的Android.mk所在路徑。
搜索可知,libmedia_jni.so位于/frameworks/base/media/jni/Android.mk。用前面實例(一)中的知識來查看相應的文件和方法名分別為:
android_media_MediaPlayer.cpp android_media_MediaPlayer_native_init()再然后,你會發現果然在該Android.mk所在目錄/frameworks/base/media/jni/中找到android_media_MediaPlayer.cpp文件,并在文件中存在相應的方法:
static void android_media_MediaPlayer_native_init(JNIEnv *env) {jclass clazz;clazz = env->FindClass("android/media/MediaPlayer");fields.context = env->GetFieldID(clazz, "mNativeContext", "J");... }Tips:MediaPlayer.java中的native_init方法所對應的native方法位于/frameworks/base/media/jni/目錄下的android_media_MediaPlayer.cpp文件中的android_media_MediaPlayer_native_init方法。
2.3 小結
JNI作為連接Java世界和C/C++世界的橋梁,很有必要掌握??赐瓯疚?#xff0c;至少能掌握在分析Android源碼過程中如何查找native方法。首先要明白native方法名和文件名的命名規律,其次要懂得該如何去搜索代碼。 JNI方式注冊無非是Android系統啟動過程中Zygote注冊以及通過System.loadLibrary方式注冊,對于系統啟動過程注冊的,可以通過查詢AndroidRuntime.cpp中的gRegJNI是否存在對應的register方法,如果不存在,則大多數情況下是通過LoadLibrary方式來注冊。
三、 JNI原理分析
再進一步來分析,Java層與native層方法是如何注冊并映射的,繼續以MediaPlayer為例。
在文件MediaPlayer.java中調用System.loadLibrary("media_jni")把libmedia_jni.so動態庫加載到內存。接下來,以loadLibrary為起點展開JNI注冊流程的過程分析。
3.1 loadLibrary
[System.java]
public static void loadLibrary(String libName) {//接下來調用Runtime方法Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader()); }[Runtime.java]
void loadLibrary(String libraryName, ClassLoader loader) {//loader不會空,則進入該分支if (loader != null) {//查找庫所在路徑String filename = loader.findLibrary(libraryName);if (filename == null) {throw new UnsatisfiedLinkError(loader + " couldn't find \"" +System.mapLibraryName(libraryName) + "\"");}//加載庫String error = doLoad(filename, loader);if (error != null) {throw new UnsatisfiedLinkError(error);}return;}//loader為空,則會進入該分支String filename = System.mapLibraryName(libraryName);List<String> candidates = new ArrayList<String>();String lastError = null;for (String directory : mLibPaths) {String candidate = directory + filename;candidates.add(candidate);if (IoUtils.canOpenReadOnly(candidate)) {//加載庫String error = doLoad(candidate, loader);if (error == null) {return;//加載成功}lastError = error;}}if (lastError != null) {throw new UnsatisfiedLinkError(lastError);}throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates); }真正加載的工作是由doLoad(),該方法內部增加同步鎖,保證并發時一致性。
private String doLoad(String name, ClassLoader loader) {...synchronized (this) {return nativeLoad(name, loader, ldLibraryPath);} }nativeLoad()這是一個native方法,再進入ART虛擬機java_lang_Runtime.cc,再細講就要深入剖析虛擬機內部,這里就不再往下深入了,后續博主有空再展開art虛擬機系列的文章,這里直接說結論:
- 調用dlopen函數,打開一個so文件并創建一個handle;
- 調用dlsym()函數,查看相應so文件的JNI_OnLoad()函數指針,并執行相應函數。
總之,System.loadLibrary()的作用就是調用相應庫中的JNI_OnLoad()方法。接下來說說JNI_OnLoad()過程。
3.2 JNI_OnLoad
[-> android_media_MediaPlayer.cpp]
jint JNI_OnLoad(JavaVM* vm, void* reserved) {JNIEnv* env = NULL;//【見3.3】 注冊JNI方法if (register_android_media_MediaPlayer(env) < 0) {goto bail;}... }3.3 register_android_media_MediaPlayer
[-> android_media_MediaPlayer.cpp]
static int register_android_media_MediaPlayer(JNIEnv *env) {//【見3.4】return AndroidRuntime::registerNativeMethods(env,"android/media/MediaPlayer", gMethods, NELEM(gMethods)); }其中gMethods,記錄java層和C/C++層方法的一一映射關系。
static JNINativeMethod gMethods[] = {{"prepare", "()V", (void *)android_media_MediaPlayer_prepare},{"_start", "()V", (void *)android_media_MediaPlayer_start},{"_stop", "()V", (void *)android_media_MediaPlayer_stop},{"seekTo", "(I)V", (void *)android_media_MediaPlayer_seekTo},{"_release", "()V", (void *)android_media_MediaPlayer_release},{"native_init", "()V", (void *)android_media_MediaPlayer_native_init},... };這里涉及到結構體JNINativeMethod,其定義在jni.h文件:
typedef struct {const char* name; //Java層native函數名const char* signature; //Java函數簽名,記錄參數類型和個數,以及返回值類型void* fnPtr; //Native層對應的函數指針 } JNINativeMethod;關于函數簽名signature在下一小節展開說明。
3.4 registerNativeMethods
[-> AndroidRuntime.cpp]
int AndroidRuntime::registerNativeMethods(JNIEnv* env,const char* className, const JNINativeMethod* gMethods, int numMethods) {//【見3.5】return jniRegisterNativeMethods(env, className, gMethods, numMethods); }jniRegisterNativeMethods該方法是由Android JNI幫助類JNIHelp.cpp來完成。
3.5 jniRegisterNativeMethods
[-> JNIHelp.cpp]
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,const JNINativeMethod* gMethods, int numMethods) {JNIEnv* e = reinterpret_cast<JNIEnv*>(env);scoped_local_ref<jclass> c(env, findClass(env, className));if (c.get() == NULL) {e->FatalError("");//無法查找native注冊方法}//【見3.6】 調用JNIEnv結構體的成員變量if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {e->FatalError("");//native方法注冊失敗}return 0; }3.6 RegisterNatives
[-> jni.h]
struct _JNIEnv {const struct JNINativeInterface* functions;jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods){ return functions->RegisterNatives(this, clazz, methods, nMethods); }... }functions是指向JNINativeInterface結構體指針,也就是將調用下面方法:
struct JNINativeInterface {jint (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,jint);... }再往下深入就到了虛擬機內部吧,這里就不再往下深入了。 總之,這個過程完成了gMethods數組中的方法的映射關系,比如java層的native_init()方法,映射到native層的android_media_MediaPlayer_native_init()方法。
虛擬機相關的變量中有兩個非常重要的量JavaVM和JNIEnv:
- JavaVM:是指進程虛擬機環境,每個進程有且只有一個JavaVM實例
- JNIEnv:是指線程上下文環境,每個線程有且只有一個JNIEnv實例,
四、JNI資源
JNINativeMethod結構體中有一個字段為signature(簽名),再介紹signature格式之前需要掌握各種數據類型在Java層、Native層以及簽名所采用的簽名格式。
4.1 數據類型
4.1.1 基本數據類型
| B | byte | jbyte |
| C | char | jchar |
| D | double | jdouble |
| F | float | jfloat |
| I | int | jint |
| S | short | jshort |
| J | long | jlong |
| Z | boolean | jboolean |
| V | void | void |
4.1.2 數組數據類型
數組簡稱則是在前面添加[:
| [B | byte[] | jbyteArray |
| [C | char[] | jcharArray |
| [D | double[] | jdoubleArray |
| [F | float[] | jfloatArray |
| [I | int[] | jintArray |
| [S | short[] | jshortArray |
| [J | long[] | jlongArray |
| [Z | boolean[] | jbooleanArray |
4.1.3 復雜數據類型
對象類型簡稱:L+classname +;
| Ljava/lang/String; | String | jstring |
| L+classname +; | 所有對象 | jobject |
| [L+classname +; | Object[] | jobjectArray |
| Ljava.lang.Class; | Class | jclass |
| Ljava.lang.Throwable; | Throwable | jthrowable |
4.1.4 Signature
有了前面的鋪墊,那么再來通過實例說說函數簽名:?(輸入參數...)返回值參數,這里用到的便是前面介紹的Signature格式。
| void foo() | ()V |
| float foo(int i) | (I)F |
| long foo(int[] i) | ([I)J |
| double foo(Class c) | (Ljava/lang/Class;)D |
| boolean foo(int[] i,String s) | ([ILjava/lang/String;)Z |
| String foo(int i) | (I)Ljava/lang/String; |
4.2 其他
(一)垃圾回收?對于Java開發人員來說無需關系垃圾回收,完全由虛擬機GC來負責垃圾回收,而對于JNI開發人員,對于內存釋放需要謹慎處理,需要的時候申請,使用完記得釋放內容,以免發生內存泄露。在JNI提供了三種Reference類型,Local Reference(本地引用), Global Reference(全局引用), Weak Global Reference(全局弱引用)。其中Global Reference如果不主動釋放,則一直不會釋放;對于其他兩個類型的引用都是釋放的可能性,那是不是意味著不需要手動釋放呢?答案是否定的,不管是這三種類型的那種引用,都盡可能在某個內存不再需要時,立即釋放,這對系統更為安全可靠,以減少不可預知的性能與穩定性問題。
另外,ART虛擬機在GC算法有所優化,為了減少內存碎片化問題,在GC之后有可能會移動對象內存的位置,對于Java層程序并沒有影響,但是對于JNI程序可要小心了,對于通過指針來直接訪問內存對象是,Dalvik能正確運行的程序,ART下未必能正常運行。
(二)異常處理
Java層出現異常,虛擬機會直接拋出異常,這是需要try..catch或者繼續往外throw。但是對于JNI出現異常時,即執行到JNIEnv中某個函數異常時,并不會立即拋出異常來中斷程序的執行,還可以繼續執行內存之類的清理工作,直到返回到Java層時才會拋出相應的異常。
另外,Dalvik虛擬機有些情況下JNI函數出錯可能返回NULL,但ART虛擬機在出錯時更多的是拋出異常。這樣導致的問題就可能是在Dalvik版本能正常運行的程序,在ART虛擬機上由于沒有正確處理異常而崩潰。
總結
本文主要通過實例,基于Android 6.0源碼來分析JNI原理,講述JNI核心功能:
- 介紹了如何查找JNI方法,讓大家明白如何從Java層跳轉到Native層;
- 分析了JNI函數注冊流程,進一步加深對JNI的理解;
- 列舉Java與native以及函數簽名方式。
總結
以上是生活随笔為你收集整理的Android JNI原理分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android Broadcast广播机
- 下一篇: 理解Android进程创建流程