Dalvik虛擬機JNI方法的註冊過程分析

       在前面一文中,我們分析了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,歡迎關注!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章