在前面一文中,我們分析了Dalvik虛擬機的運行過程。從中可以知道,Dalvik虛擬機在調用一個成員函數的時候,如果發現該成員函數是一個JNI方法,那麼就會直接跳到它的地址去執行。也就是說,JNI方法是直接在本地操作系統上執行的,而不是由Dalvik虛擬機解釋器執行。由此也可看出,JNI方法是Android應用程序與本地操作系統直接進行通信的一個手段。在本文中,我們就詳細分析JNI方法的註冊過程。
老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!
在Android系統中,JNI方法是以C/C++語言來實現的,然後編譯在一個SO文件裏面。這個JNI方法在能夠被調用之前,首先要加載到當前應用程序進程的地址空間來,如下所示:
package shy.luo.jni; public class ClassWithJni { ...... static { System.loadLibrary("nanosleep"); } ...... private native int nanosleep(long seconds, long nanoseconds); ...... }
上述代碼假設ClassWithJni類有一個JNI方法nanosleep,它實現在一個名稱爲libnanosleep.so的文件中,因此,在該JNI方法能夠被調用之前,我們首先要將它加載到當前應用程序進程來,這是通過調用System類的靜態成員函數loadLibrary來實現的。
JNI方法nanosleep的實現如下所示:
#include "jni.h" #include "JNIHelp.h" #include <time.h> static jint shy_luo_jni_ClassWithJni_nanosleep(JNIEnv* env, jobject clazz, jlong seconds, jlong nanoseconds) { struct timespec req; req.tv_sec = seconds; req.tv_nsec = nanoseconds; return nanosleep(&req, NULL); } static const JNINativeMethod method_table[] = { {"nanosleep", "(JJ)I", (void*)shy_luo_jni_ClassWithJni_nanosleep}, }; extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env = NULL; jint result = -1; if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { return result; } jniRegisterNativeMethods(env, "shy/luo/jni/ClassWithJni", method_table, NELEM(method_table)); return JNI_VERSION_1_4; }
假設上述函數經過編譯之後,就位於一個名稱爲libnanosleep.so的文件。
當libnanosleep.so文件被加載的時候,函數JNI_OnLoad就會被調用。在函數JNI_OnLoad中,參數vm描述的是當前進程中的Dalvik虛擬機,通過調用它的成員函數GetEnv就可以獲得一個JNIEnv對象。有了這個JNIEnv對象之後,我們就可以調用另外一個函數jniRegisterNativeMethods來向當前進程中的Dalvik虛擬機註冊一個JNI方法shy_luo_jni_ClassWithJni_nanosleep。這個JNI方法即爲shy.luo.jni.ClassWithJni類的成員函數nanasleep的實現。
JNI方法shy_luo_jni_ClassWithJni_nanosleep要做的事情實際上就是通過系統調用nanosleep來使得當前進程進入睡眠狀態,直至seconds秒nanoseconds納秒之後再喚醒。使用系統調用nanosleep來使得當前進程進入睡眠狀態的好處它的時間精度可以達到納秒級,但是這個系統調用有兩個地方是需要注意的:
1. 如果進程在睡眠的過程中接收到信號,那麼它就會提前被喚醒,這時候系統調用nanosleep的返回值爲-1,並且錯誤代碼errno被設置爲EINTR。
2. 如果CPU的時鐘中斷精度達不到納秒級別,那麼nanosleep的睡眠精度也達不到納秒級,也就是說,當前進程不一定能在指定的納秒之後被喚醒,會有一定的延時。
不過,JNI方法shy_luo_jni_ClassWithJni_nanosleep的實現不是我們的重點,我們的重點是分析它註冊到Dalvik虛擬機的過程。
前面提到,JNI方法shy_luo_jni_ClassWithJni_nanosleep是libnanosleep.so文件加載的時候被註冊到Dalvik虛擬機的,因此,我們就從libnanosleep.so文件的加載開始,分析JNI方法shy_luo_jni_ClassWithJni_nanosleep註冊到Dalvik虛擬機的過程,也就是從System類的靜態成員函數loadLibrary開始分析一個JNI方法註冊到Dalvik虛擬機的過程,如圖1所示:
圖1 JNI方法註冊到Dalvik虛擬機的過程
這個過程可以分爲12個步驟,接下來我們就詳細分析每一個步驟。
Step 1. System.loadLibrary
public final class System { ...... public static void loadLibrary(String libName) { SecurityManager smngr = System.getSecurityManager(); if (smngr != null) { smngr.checkLink(libName); } Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader()); } ...... }
這個函數定義在文件libcore/luni/src/main/java/java/lang/System.java中。
System類的成員函數loadLibrary首先調用SecurityManager類的成員函數checkLink來進行安全檢查,即檢查名稱爲libName的so文件是否允許加載。注意,這是Java的安全代碼檢查機制,而不是Android系統的安全檢查機制,而且Android系統沒有使用它來進行安全檢查。因此,這個檢查總是能通過的。
System類的成員函數loadLibrary接下來就再通過運行時類Runtime的成員函數loadLibrary來加載名稱爲libName的so文件,接下來我們就繼續分析它的實現。
Step 2. Runtime.loadLibrary
public class Runtime { ...... void loadLibrary(String libraryName, ClassLoader loader) { if (loader != null) { String filename = loader.findLibrary(libraryName); if (filename == null) { throw new UnsatisfiedLinkError("Couldn't load " + libraryName + ": " + "findLibrary returned null"); } String error = nativeLoad(filename, loader); if (error != null) { throw new UnsatisfiedLinkError(error); } return; } 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 (new File(candidate).exists()) { String error = nativeLoad(candidate, loader); if (error == null) { return; // We successfully loaded the library. Job done. } lastError = error; } } if (lastError != null) { throw new UnsatisfiedLinkError(lastError); } throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates); } ...... }
這個函數定義在文件libcore/luni/src/main/java/java/lang/Runtime.java中。
在Runtime類的成員函數loadLibrary中,參數libraryName表示要加載的so文件,而參數loader表示與要加載的so文件所關聯的類的一個類加載器。例如,在我們這個情景中,libraryName等於“nanosleep”,與它所關聯的類爲shy.luo.jni.ClassWithJni。每一類有一個關聯的類加載器,用來負責加載該類。在Dalvik虛擬機中,類加載器除了知道它要加載的類所在的文件路徑之外,還知道該類所屬的APK用來保存so文件的路徑。因此,給定一個so文件名稱,一個類加載器可以判斷它是否存在自己的so文件目錄中。
參數libraryName只是描述要加載的so文件的部分名稱,它的完整名稱需要根據本地操作系統的特證來確定。由於目前Android系統都是屬於Linux系統,而在Linux系統中,so文件的命名規範通常就是lib<name>.so的形式,因此,在我們這個情景中,名稱爲“nanosleep”的so文件的完整名稱就爲“libnanosleep.so”,這是通過調用System類的靜態成員函數mapLibraryName來獲得的。
上面所獲得的libnanosleep.so文件的名稱仍然還不夠完整,因爲它沒有包含絕對路徑。在這種情況下,我們是無法將它加載到Dalvik虛擬機中去的。當參數loader的值不等於null的時候,Runtime類的成員函數loadLibrary就會調用它的成員函數findLibrary來它的so文件目錄中尋找是否有一外名稱爲“libnanosleep.so”。如果存在的話,那麼就會返回該libnanosleep.so文件的絕對路徑。有了libnanosleep.so文件的絕對路徑之後,就可以調用Runtime類的另外一個成員函數nativeLoad來將它加載到當前進程的Dalvik虛擬機中。注意,將參數libraryName轉換爲lib<name>.so的完整形式,以及獲得該so文件的絕對路徑,都是由參數loader所描述的一個類加載器的成員函數findLibrary來完成的。
另一方面,如果參數loader的值等於null,那麼就表示當前要加載的so文件要在系統範圍的so文件目錄查找。這些系統範圍的so文件目錄保存在Runtime類的成員變量mLibPaths所描述的一個String數組中。通過依次檢查這些目錄是否存在與參數libraryName對應的so文件,就可以確定參數libraryName所指定加載的so文件是否是一個合法的so文件。如果合法的話,那麼同樣會調用Runtime類的另外一個成員函數nativeLoad來將它加載到當前進程的Dalvik虛擬機中。注意,這裏在檢查參數libraryName所表示的so文件是否存在於系統範圍的so文件目錄之前,同樣要將它轉換爲lib<name>.so的形式,這同樣也是通過調用System類的靜態成員函數mapLibraryName來完成的。
如果最後無法在指定的APK或者系統範圍的so文件目錄中找到由參數libraryName所描述的so文件,或者找到了該so文件,但是在加載該so文件的過程中出現錯誤,那麼Runtime類的成員函數loadLibrary都會拋出一個類型爲UnsatisfiedLinkError的異常。
由於加載參數libraryName所描述的so文件是由Runtime類的成員函數nativeLoad來實現的,因此,接下來我們繼續分析它的實現。
Step 3. Runtime.nativeLoad
public class Runtime { ...... private static native String nativeLoad(String filename, ClassLoader loader); ...... }
這個函數定義在文件libcore/luni/src/main/java/java/lang/Runtime.java中。
Runtime類的成員函數nativeLoad是一個JNI方法。由於該JNI方法是屬於Java核心類Runtime的,也就是說,它在Dalvik虛擬機啓動的時候就已經在內部註冊過了,因此,這時候我們可以直接調用它註冊其它的JNI方法,也就是so文件filename裏面所指定的JNI方法。Dalvik虛擬機在啓動過程中註冊Java核心類的操作,具體可以參考前面Dalvik虛擬機的啓動過程分析一文。
Runtime類的成員函數nativeLoad在C++層對應的函數爲Dalvik_java_lang_Runtime_nativeLoad,如下所示:
static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args, JValue* pResult) { StringObject* fileNameObj = (StringObject*) args[0]; Object* classLoader = (Object*) args[1]; char* fileName = NULL; StringObject* result = NULL; char* reason = NULL; bool success; assert(fileNameObj != NULL); fileName = dvmCreateCstrFromString(fileNameObj); success = dvmLoadNativeCode(fileName, classLoader, &reason); if (!success) { const char* msg = (reason != NULL) ? reason : "unknown failure"; result = dvmCreateStringFromCstr(msg); dvmReleaseTrackedAlloc((Object*) result, NULL); } free(reason); free(fileName); RETURN_PTR(result); }
這個函數定義在文件dalvik/vm/native/java_lang_Runtime.c中。
參數args[0]保存的是一個Java層的String對象,這個String對象描述的就是要加載的so文件,函數Dalvik_java_lang_Runtime_nativeLoad首先是調有函數dvmCreateCstrFromString來將它轉換成一個C++層的字符串fileName,然後再調用函數dvmLoadNativeCode來執行加載so文件的操作。
接下來,我們就繼續分函數dvmLoadNativeCode的實現,以便可以瞭解一個so文件的加載過程。
Step 4. dvmLoadNativeCode
bool dvmLoadNativeCode(const char* pathName, Object* classLoader, char** detail) { SharedLib* pEntry; void* handle; ...... pEntry = findSharedLibEntry(pathName); if (pEntry != NULL) { if (pEntry->classLoader != classLoader) { ...... return false; } ...... if (!checkOnLoadResult(pEntry)) return false; return true; } ...... handle = dlopen(pathName, RTLD_LAZY); ...... /* create a new entry */ SharedLib* pNewEntry; pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib)); pNewEntry->pathName = strdup(pathName); pNewEntry->handle = handle; pNewEntry->classLoader = classLoader; ...... /* try to add it to the list */ SharedLib* pActualEntry = addSharedLibEntry(pNewEntry); if (pNewEntry != pActualEntry) { ...... freeSharedLibEntry(pNewEntry); return checkOnLoadResult(pActualEntry); } else { ...... bool result = true; void* vonLoad; int version; vonLoad = dlsym(handle, "JNI_OnLoad"); if (vonLoad == NULL) { LOGD("No JNI_OnLoad found in %s %p, skipping init\n", pathName, classLoader); } else { ...... OnLoadFunc func = vonLoad; ...... version = (*func)(gDvm.vmList, NULL); ...... if (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 && version != JNI_VERSION_1_6) { ....... result = false; } else { LOGV("+++ finished JNI_OnLoad %s\n", pathName); } } ...... if (result) pNewEntry->onLoadResult = kOnLoadOkay; else pNewEntry->onLoadResult = kOnLoadFailed; ...... return result; } }
這個函數定義在文件dalvik/vm/Native.c中。
函數dvmLoadNativeCode首先是檢查參數pathName所指定的so文件是否已經加載過了,這是通過調用函數findSharedLibEntry來實現的。如果已經加載過,那麼就可以獲得一個SharedLib對象pEntry。這個SharedLib對象pEntry描述了有關參數pathName所指定的so文件的加載信息,例如,上次用來加載它的類加載器和上次的加載結果。如果上次用來加載它的類加載器不等於當前所使用的類加載器,或者上次沒有加載成功,那麼函數dvmLoadNativeCode就回直接返回false給調用者,表示不能在當前進程中加載參數pathName所描述的so文件。
我們假設參數pathName所指定的so文件還沒有被加載過,這時候函數dvmLoadNativeCode就會先調用dlopen來在當前進程中加載它,並且將獲得的句柄保存在變量handle中,接着再創建一個SharedLib對象pNewEntry來描述它的加載信息。這個SharedLib對象pNewEntry還會通過函數addSharedLibEntry被緩存起來,以便可以知道當前進程都加載了哪些so文件。
注意,在調用函數addSharedLibEntry來緩存新創建的SharedLib對象pNewEntry的時候,如果得到的返回值pActualEntry指向的不是SharedLib對象pNewEntry,那麼就表示另外一個線程也正在加載參數pathName所指定的so文件,並且比當前線程提前加載完成。在這種情況下,函數addSharedLibEntry就什麼也不用做而直接返回了。否則的話,函數addSharedLibEntry就要繼續負責調用前面所加載的so文件中的一個指定的函數來註冊它裏面的JNI方法。
這個指定的函數的名稱爲“JNI_OnLoad”,也就是說,每一個用來實現JNI方法的so文件都應該定義有一個名稱爲“JNI_OnLoad”的函數,並且這個函數的原型爲:
jint JNI_OnLoad(JavaVM* vm, void* reserved);
函數dvmLoadNativeCode通過調用函數dlsym就可以獲得在前面加載的so中名稱爲“JNI_OnLoad”的函數的地址,最終保存在函數指針func中。有了這個函數指針之後,我們就可以直接調用它來執行註冊JNI方法的操作了。注意,在調用該JNI_OnLoad函數時,第一個要傳遞進行的參數是一個JavaVM對象,這個JavaVM對象描述的是在當前進程中運行的Dalvik虛擬機,第二個要傳遞的參數可以設置爲NULL,這是保留給以後使用的。
從前面Dalvik虛擬機的啓動過程分析一文可以知道,在當前進程所運行的Dalvik虛擬機實例是通過全局變量gDvm所描述的一個DvmGlobals結構體的成員變量vmList來描述的,因此,我們就可以將它傳遞在前面加載的so中名稱中定義的JNI_OnLoad函數。注意,定義在該so文件中的JNI_OnLoad函數一旦執行成功,它的返回值就必須等於JNI_VERSION_1_2、JNI_VERSION_1_4或者JNI_VERSION_1_6,用來表示所註冊的JNI方法的版本。
最後, 函數dvmLoadNativeCode根據上述的JNI_OnLoad函數的執行成功與否,將前面所創建的一個SharedLib對象pNewEntry的成員變量onLoadResult設置爲kOnLoadOkay或者kOnLoadFailed,這樣就可以記錄參數pathName所指定的so文件是否是加載成功的,也就是它是否成功地註冊了其內部的JNI方法。
在我們這個情景中,參數pathName所指定的so文件爲libnanosleep.so,接下來我們就繼續分析它的函數JNI_OnLoad的實現,以便可以發解定義在它裏面的JNI方法的註冊過程。
Step 5. JNI_OnLoad
定義在libnanosleep.so文件中的函數JNI_OnLoad的實現可以參考文章開始的部分。從它的實現可以知道,它所註冊的JNI方法shy_luo_jni_ClassWithJni_nanosleep是與shy.luo.jni.ClassWithJni類的成員函數nanosleep對應的,並且是通過調用函數jniRegisterNativeMethods來實現的。因此,接下來我們就繼續分析函數jniRegisterNativeMethods的實現。
Step 6. jniRegisterNativeMethods
int jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { jclass clazz; LOGV("Registering %s natives\n", className); clazz = (*env)->FindClass(env, className); if (clazz == NULL) { LOGE("Native registration unable to find class '%s'\n", className); return -1; } int result = 0; if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) { LOGE("RegisterNatives failed for '%s'\n", className); result = -1; } (*env)->DeleteLocalRef(env, clazz); return result; }
這個函數定義在文件dalvik/libnativehelper/JNIHelp.c中。
參數env所指向的一個JNIEnv結構體,通過調用這個JNIEnv結構體可以獲得參數className所描述的一個類。這個類就是要註冊JNI的類,而它所要註冊的JNI就是由參數gMethods來描述的。
註冊參數gMethods所描述的JNI方法是通過調用env所指向的一個JNIEnv結構體的成員函數RegisterNatives來實現的,因此,接下來我們就繼續分析它的實現。
Step 7. JNIEnv.RegisterNatives
typedef _JNIEnv JNIEnv; ...... struct _JNIEnv { /* do not rename this; it does not seem to be entirely opaque */ const struct JNINativeInterface* functions; ...... jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods) { return functions->RegisterNatives(this, clazz, methods, nMethods); } ...... }
這個函數定義在文件dalvik/libnativehelper/include/nativehelper/jni.h中。
從前面Dalvik虛擬機的運行過程分析一文可以知道,結構體JNIEnv的成員變量functions指向的是一個函數表,這個函數表又包含了一系列的函數指針,指向了在當前進程中運行的Dalvik虛擬機中定義的函數。對於結構體JNIEnv的成員函數RegisterNatives來說,它就是通過調用這個函數表中名稱爲RegisterNatives的函數指針來註冊參數gMethods所描述的JNI方法的。
從前面Dalvik虛擬機的啓動過程分析一文可以知道,上述函數表中名稱爲RegisterNatives的函數指針指向的是在Dalvik虛擬機內部定義的函數RegisterNatives,因此,接下來我們就繼續分析它的實現。
Step 8. RegisterNatives
static jint RegisterNatives(JNIEnv* env, jclass jclazz, const JNINativeMethod* methods, jint nMethods) { JNI_ENTER(); ClassObject* clazz = (ClassObject*) dvmDecodeIndirectRef(env, jclazz); jint retval = JNI_OK; int i; ...... for (i = 0; i < nMethods; i++) { if (!dvmRegisterJNIMethod(clazz, methods[i].name, methods[i].signature, methods[i].fnPtr)) { retval = JNI_ERR; } } JNI_EXIT(); return retval; }
這個函數定義在文件dalvik/vm/Jni.c中。
參數jclazz描述的是要註冊JNI方法的類,而參數methods描述的是要註冊的一組JNI方法,這個組JNI方法的個數由參數nMethods來描述。
函數RegisterNatives首先是調用函數dvmDecodeIndirectRef來獲得要註冊JNI方法的類對象,接着再通過一個for循環來依次調用函數dvmRegisterJNIMethod註冊參數methods描述所描述的每一個JNI方法。注意,每一個JNI方法都由名稱、簽名和地址來描述。
接下來,我們就繼續分析函數dvmRegisterJNIMethod的實現。
Step 9. dvmRegisterJNIMethod
static bool dvmRegisterJNIMethod(ClassObject* clazz, const char* methodName, const char* signature, void* fnPtr) { Method* method; bool result = false; if (fnPtr == NULL) goto bail; method = dvmFindDirectMethodByDescriptor(clazz, methodName, signature); if (method == NULL) method = dvmFindVirtualMethodByDescriptor(clazz, methodName, signature); if (method == NULL) { LOGW("ERROR: Unable to find decl for native %s.%s:%s\n", clazz->descriptor, methodName, signature); goto bail; } if (!dvmIsNativeMethod(method)) { LOGW("Unable to register: not native: %s.%s:%s\n", clazz->descriptor, methodName, signature); goto bail; } if (method->nativeFunc != dvmResolveNativeMethod) { /* this is allowed, but unusual */ LOGV("Note: %s.%s:%s was already registered\n", clazz->descriptor, methodName, signature); } dvmUseJNIBridge(method, fnPtr); LOGV("JNI-registered %s.%s:%s\n", clazz->descriptor, methodName, signature); result = true; bail: return result; }
這個函數定義在文件dalvik/vm/Jni.c中。
函數dvmRegisterJNIMethod在註冊參數methodName所描述的JNI方法之前,首先會進行一系列的檢查,包括:
1. 確保參數clazz所描述的類有一個名稱爲methodName的成員函數。首先是調用函數dvmFindDirectMethodByDescriptor來檢查methodName是否是clazz的一個非虛成員函數,然後再調用函數dvmFindVirtualMethodByDescriptor來檢查methodName是否是clazz的一個虛成員函數。
2. 確保類clazz的成員函數methodName確實是聲明爲JNI方法,即帶有native修飾符,這是通過調用函數dvmIsNativeMethod來實現的。
通過了前面的第1個檢查之後,就可以獲得一個Method對象method,用來描述要註冊的JNI方法所對應的Java類成員函數。當一個Method對象method描述的是一個JNI方法的時候,它的成員變量nativeFunc保存的就是該JNI方法的地址,但是在對應的JNI方法註冊進來之前,該成員變量的值被統一設置爲dvmResolveNativeMethod。因此,當我們調用了一個未註冊的JNI方法時,實際上執行的是函數dvmResolveNativeMethod。函數dvmResolveNativeMethod此時會在Dalvik虛擬內部以及當前所有已經加載的共享庫中檢查是否存在對應的JNI方法。如果不存在,那麼它就會拋出一個類型爲java.lang.UnsatisfiedLinkError的異常。
注意,一個JNI方法是可以重複註冊的,無論如何,函數dvmRegisterJNIMethod都是調用另外一個函數dvmUseJNIBridge來繼續執行註冊JNI的操作。
Step 10. dvmUseJNIBridge
/** * Returns true if the -Xjnitrace setting implies we should trace 'method'. */ static bool shouldTrace(Method* method) { return gDvm.jniTrace && strstr(method->clazz->descriptor, gDvm.jniTrace); } /* * Point "method->nativeFunc" at the JNI bridge, and overload "method->insns" * to point at the actual function. */ void dvmUseJNIBridge(Method* method, void* func) { DalvikBridgeFunc bridge = shouldTrace(method) ? dvmTraceCallJNIMethod : dvmSelectJNIBridge(method); dvmSetNativeFunc(method, bridge, func); }
這個函數定義在文件dalvik/vm/Jni.c中。
一個JNI方法並不是直接被調用的,而是通過由Dalvik虛擬機間接地調用,這個用來間接調用JNI方法的函數就稱爲一個Bridge。這些Bridage函數在真正調用JNI方法之前,會執行一些通用的初始化工作。例如,會將當前線程的狀態設置爲NATIVE,因爲它即將要執行一個Native函數。又如,會爲即將要被調用的JNI方法準備好前面兩個參數,第一個參數是一個JNIEnv對象,用來描述當前線程的Java環境,通過它可以訪問反過來訪問Java代碼和Java對象,第二個參數是一個jobject對象,用來描述當前正在執行JNI方法的Java對象。
這些Bridage函數實際上仍然不是直接調用地調用JNI方法的,這是因爲Dalvik虛擬機是可以運行在各種不同的平臺之上,而每一種平臺可能都定義有自己的一套函數調用規範,也就是所謂的ABI(Application Binary Interface),這是一個API(Application Programming Interface)不同的概念。ABI是在二進制級別上定義的一套函數調用規範,例如參數是通過寄存器來傳遞還是堆棧來傳遞,而API定義是一個應用程序編程接口規範。換句話說,API定義了源代碼和庫之間的接口,因此同樣的代碼可以在支持這個API的任何系統中編譯 ,而ABI允許編譯好的目標代碼在使用兼容ABI的系統中無需改動就能運行。
爲了使得運行在不同平臺上的Dalvik虛擬機能夠以統一的方法來調用JNI方法,這些Bridage函數使用了一個libffi庫,它的源代碼位於external/libffi目錄中。Libffi是一個開源項目,用於高級語言之間的相互調用的處理,它的實現機制可以進一步參考http://www.sourceware.org/libffi/。
回到函數dvmUseJNIBridge中,它主要就是根據Dalvik虛擬機的啓動選項來爲即將要註冊的JNI選擇一個合適的Bridge函數。如果我們在Dalvik虛擬機啓動的時候,通過-Xjnitrace選項來指定了要跟蹤參數method所描述的JNI方法,那麼函數dvmUseJNIBridge爲該JNI方法選擇的Bridge函數就爲dvmTraceCallJNIMethod,否則的話,就再通過另外一個函數dvmSelectJNIBridge來進一步選擇一個合適的Bridge函數。選擇好Bridge函數之後,函數dvmUseJNIBridge最終就調用函數dvmSetNativeFunc來執行真正的JNI方法註冊操作。
我們假設參數method所描述的JNI方法沒有設置爲跟蹤,因此,接下來,我們就首先分析函數dvmSelectJNIBridge的實現,接着再分析函數dvmSetNativeFunc的實現。
Step 11. dvmSelectJNIBridge
/* * Returns the appropriate JNI bridge for 'method', also taking into account * the -Xcheck:jni setting. */ static DalvikBridgeFunc dvmSelectJNIBridge(const Method* method) { enum { kJNIGeneral = 0, kJNISync = 1, kJNIVirtualNoRef = 2, kJNIStaticNoRef = 3, } kind; static const DalvikBridgeFunc stdFunc[] = { dvmCallJNIMethod_general, dvmCallJNIMethod_synchronized, dvmCallJNIMethod_virtualNoRef, dvmCallJNIMethod_staticNoRef }; static const DalvikBridgeFunc checkFunc[] = { dvmCheckCallJNIMethod_general, dvmCheckCallJNIMethod_synchronized, dvmCheckCallJNIMethod_virtualNoRef, dvmCheckCallJNIMethod_staticNoRef }; bool hasRefArg = false; if (dvmIsSynchronizedMethod(method)) { /* use version with synchronization; calls into general handler */ kind = kJNISync; } else { /* * Do a quick scan through the "shorty" signature to see if the method * takes any reference arguments. */ const char* cp = method->shorty; while (*++cp != '\0') { /* pre-incr to skip return type */ if (*cp == 'L') { /* 'L' used for both object and array references */ hasRefArg = true; break; } } if (hasRefArg) { /* use general handler to slurp up reference args */ kind = kJNIGeneral; } else { /* virtual methods have a ref in args[0] (not in signature) */ if (dvmIsStaticMethod(method)) kind = kJNIStaticNoRef; else kind = kJNIVirtualNoRef; } } return dvmIsCheckJNIEnabled() ? checkFunc[kind] : stdFunc[kind]; }
這個函數定義在文件dalvik/vm/Jni.c中。
Dalvik虛擬機提供的Bridge函數主要是分爲兩類。第一類Bridge函數在調用完成JNI方法之後,會檢查該JNI方法的返回結果是否與聲明的一致,這是因爲一個聲明返回String的JNI方法在執行時返回的可能會是一個Byte Array。如果不一致,取決於Dalvik虛擬機的啓動選項,它可能會停機。第二類Bridge函數不對JNI方法的返回結果進行上述檢查。選擇哪一類Bridge函數可以通過-Xcheck:jni選項來決定。不過由於檢查一個JNI方法的返回結果是否與聲明的一致是很耗時的,因此,我們一般都不會使用第一類Bridge函數。
此外,每一類Bridge函數又分爲四個子類:Genernal、Sync、VirtualNoRef和StaticNoRef,它們的選擇規則爲:
1. 一個JNI方法的參數列表中如果包含有引用類型的參數,那麼對應的Bridge函數就是Genernal類型的,即爲dvmCallJNIMethod_general或者dvmCheckCallJNIMethod_general。
2. 一個JNI方法如果聲明爲同步方法,即帶有synchronized修飾符,那麼對應的Bridge函數就是Sync類型的,即爲dvmCallJNIMethod_synchronized或者dvmCheckCallJNIMethod_synchronized。
3. 一個JNI方法的參數列表中如果不包含有引用類型的參數,並且它是一個虛成員函數,那麼對應的Bridge函數就是kJNIVirtualNoRef類型的,即爲dvmCallJNIMethod_virtualNoRef或者dvmCheckCallJNIMethod_virtualNoRef。
4. 一個JNI方法的參數列表中如果不包含有引用類型的參數,並且它是一個靜態成員函數,那麼對應的Bridge函數就是StaticNoRef類型的,即爲dvmCallJNIMethod_staticNoRef或者dvmCheckCallJNIMethod_staticNoRef。
每一類Bridge函數之所以要劃分爲上述四個子類,是因爲每一個子類的Bridge函數在調用真正的JNI方法之前,所要進行的準備工作是不一樣的。例如,Genernal類型的Bridge函數需要爲引用類型的參數增加一個本地引用,避免它在JNI方法執行的過程中被回收。又如,Sync類型的Bridge函數在調用JNI方法之前,需要執行同步原始,以避免多線程訪問的競爭問題。
這一步執行完成之後,返回到前面的Step 10中,即函數dvmUseJNIBridge中,這時候它就獲得了一個Bridge函數,因此,接下來它就可以調用函數dvmSetNativeFunc來執行真正的JNI方法註冊操作了。
Step 12. dvmSetNativeFunc
void dvmSetNativeFunc(Method* method, DalvikBridgeFunc func, const u2* insns) { ...... if (insns != NULL) { /* update both, ensuring that "insns" is observed first */ method->insns = insns; android_atomic_release_store((int32_t) func, (void*) &method->nativeFunc); } else { /* only update nativeFunc */ method->nativeFunc = func; } ...... }
這個函數定義在文件dalvik/vm/oo/Class.c中。
參數method表示要註冊JNI方法的Java類成員函數,參數func表示JNI方法的Bridge函數,參數insns表示要註冊的JNI方法的函數地址。
當參數insns的值不等於NULL的時候,函數dvmSetNativeFunc就分別將參數insns和func的值分別保存在參數method所指向的一個Method對象的成員變量insns和nativeFunc中,而當insns的值等於NULL的時候,函數dvmSetNativeFunc就只將參數func的值保存在參數method所指向的一個Method對象成員變量nativeFunc中。
假設在前面的Step 11中選擇的Bridge函數爲dvmCallJNIMethod_general,並且結合前面Dalvik虛擬機的運行過程分析一文,我們就可以得到Dalvik虛擬機在運行過程中調用JNI方法的過程:
1. 調用函數dvmCallJNIMethod_general,執行一些必要的準備工作;
2. 函數dvmCallJNIMethod_general再調用函數dvmPlatformInvoke來以統一的方式來調用對應的JNI方法;
3. 函數dvmPlatformInvoke通過libffi庫來調用對應的JNI方法,以屏蔽Dalvik虛擬機運行在不同目標平臺的細節。
至此,我們就分析完成Dalvik虛擬機JNI方法的註冊過程了。這樣,我們就打通了Java代碼和Native代碼之間的道路。實際上,很多Java和Android核心類的功能都是通過本地操作系統提供的系統調用來完成的,例如,Zygote類的成員函數forkAndSpecialize最終是通過Linux系統調用fork來創建一個Android應用程序進程的,又如,Thread類的成員函數start最終是通過pthread線程庫函數pthread_create來創建一個Android應用程序線程的。
在接下來的一篇文章中,我們就在Java代碼和Native代碼打通了的基礎上,分析Android應用程序進程和線程與本地操作系統進程的線程的關係,也就是Dalvik虛擬機進程和線程與本地操作系統進程的線程的關係,敬請關注!
老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!