java JNI的兩種實現方法:javah vs JNI_OnLoad


Java JNI有兩種方法,一種是通過javah,獲取一組帶簽名函數,然後實現這些函數。這種方法很常用,也是官方推薦的方法,本文不再詳述,重點說明一下JNI_OnLoad方法。

當在系統中調用System.loadLibrary函數時,該函數會找到對應的動態庫,然後首先試圖找到"JNI_OnLoad"函數,如果該函數存在,則調用它。

JNI_OnLoad可以和JNIEnv的registerNatives函數結合起來,實現動態的函數替換。

下面用一個簡單的例子來說明

java類聲明
創建目錄mj/jnitest,並新建兩個文件MyObject.java和JniTest.java

MyObject.java

  1. package mj.jnitest;

  2. class MyObject {  

  3.    static {   

  4.       System.loadLibrary("jni");  //這是加載使用javah規定風格實現的庫

  5. }

  6. //下面定義兩個native函數

  7. public native void func1();

  8. public native void func2();

  9. }
複製代碼
JniTest.java
  1. package mj.jnitest;

  2. class JniTest {

  3.      // static {  //這是一種靜態的加載方式,可以完全工作;但是下面我們要用更靈活的方式進行

  4.               //  System.loadLibrary("jni2");

  5.     // }

  6. public static void main(String[] args)  {   

  7.             MyObject obj = new MyObject();   

  8.            //在fun2函數替換之前,先進行一次調用,會調研jni1中的函數

  9.            obj.func1();   

  10.            obj.func2();   

  11.             //用JNI_OnLoad進行主動註冊

  12.            System.loadLibrary("jni2");   

  13.            obj.func1();   

  14.            obj.func2(); //func2已經被jni2中的函數替換

  15.     }

  16. };
複製代碼
在JniTest.java中,有兩個動態庫jni1和jni2會被同時加載。jni1在MyObject類被鏈接時被加載;jni2則在MyObject的實例obj運行時被加載。首先看看他的輸出結果:

$ java mj.jnitest.JniTest

--- func1 called in version 1

--- func2 called in version 1

--- func1 called in version 1

--- func2 called in version 2

從結果看出,前兩行調用obj.func1和obj.func2,都是jni1中的函數,所以打印的是version 1;

而加載了jni2後,obj.func1函數仍舊是jni1中的,而func2就變成了jni2中的了。

下面看下jni1和jni2的源代碼

jni1的源代碼mj_jnitest_MyObject.c
  1. #include <stdio.h>
  2. #include <stdlib.h>

  3. #include "mj_jnitest_MyObject.h"

  4. /*
  5. * Class:     mj_jnitest_MyObject
  6. * Method:    func1
  7. * Signature: ()V
  8. */
  9. JNIEXPORT void JNICALL Java_mj_jnitest_MyObject_func1
  10.   (JNIEnv *env, jobject jobj)
  11. {
  12. printf("--- func1 called in version 1\n");
  13. }

  14. /*
  15. * Class:     mj_jnitest_MyObject
  16. * Method:    func2
  17. * Signature: ()V
  18. */
  19. JNIEXPORT void JNICALL Java_mj_jnitest_MyObject_func2
  20.   (JNIEnv *env, jobject jobj)
  21. {
  22. printf("--- func2 called in version 1\n");
  23. }
複製代碼
斜體部分正是打印的內容

jni2的源代碼jni2.c(部分)
include <stdlib.h>
  1. #include <jni.h>

  2. static void JNICALL func2
  3.   (JNIEnv *env, jobject jobj)
  4. {
  5. printf("--- func2 called in version 2\n");
  6. }
複製代碼
....

JNI_OnLoad的使用方法
先看一下jni2.c的完整源代碼,並注意註釋
  1. #include <stdio.h>
  2. #include <stdlib.h>

  3. #include <jni.h> //jni的主要頭文件

  4. static void JNICALL func2  //函數名字可以隨便取,不過參數一定要和javah生成的函數的參數一致,包括返回值
  5.   (JNIEnv *env, jobject jobj)
  6. {
  7. printf("--- func2 called in version 2\n");
  8. }

  9. static const JNINativeMethod gMethods[] = { //定義批量註冊的數組,是註冊的關鍵部分
  10. {"func2", "()V", (void*)func2} // func2是在java中聲明的native函數名,"()V"是函數的簽名,可以通過javah獲取。
  11. };

  12. JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void *reserved) //這是JNI_OnLoad的聲明,必須按照這樣的方式聲明
  13. {
  14. JNIEnv* env = NULL; //註冊時在JNIEnv中實現的,所以必須首先獲取它
  15. jint result = -1;

  16. if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_4) != JNI_OK) //從JavaVM獲取JNIEnv,一般使用1.4的版本
  17.   return -1;

  18. jclass clazz;
  19. static const char* const kClassName="mj/jnitest/MyObject";

  20. clazz = (*env)->FindClass(env, kClassName); //這裏可以找到要註冊的類,前提是這個類已經加載到java虛擬機中。 這裏說明,動態庫和有native方法的類之間,沒有任何對應關係。

  21. if(clazz == NULL)
  22. {
  23.   printf("cannot get class:%s\n", kClassName);
  24.   return -1;
  25. }

  26. if((*env)->RegisterNatives(env, clazz,gMethods, sizeof(gMethods)/sizeof(gMethods[0]))!= JNI_OK) //這裏就是關鍵了,把本地函數和一個java類方法關聯起來。不管之前是否關聯過,一律把之前的替換掉!
  27. {
  28.   printf("register native method failed!\n");
  29.   return -1;
  30. }

  31. return JNI_VERSION_1_4; //這裏很重要,必須返回版本,否則加載會失敗。
  32. }
複製代碼
對他進行編譯後,得到一個libjni2.so。

C++用法說明
上面的用法是c語言中的用法,在C++中更簡單。

JavaVM和JNIEnv都是經過簡單封裝的類,可以直接按照如下方式調用

(vm->GetEnv((void**)&env, JNI_VERSION_1_4)

env->FindClass(kClassName);

env->RegisterNatives(clazz,gMethods, sizeof(gMethods)/sizeof(gMethods[0]))

Dalvik中動態庫的原理簡要分析

之所以出現這種結果,和jni的機制有關的,通過對Android中的Dalvik的分析,可以印證。

System.loadLibrary,也是一個native方法,它向下調用的過程是:

Dalvik/vm/native/java_lang_Runtime.cpp: Dalvik_java_lang_Runtime_nativeLoad ->

    Dalvik/vm/Native.cpp:dvmLoadNativeCode

dvmLoadNativeCode
打開函數dvmLoadNativeCode,可以找到以下代碼
  1. bool result = true;
  2.         void* vonLoad;
  3.         int version;

  4.         vonLoad = dlsym(handle, "JNI_OnLoad"); //獲取JNI_OnLoad的地址
  5.         if (vonLoad == NULL) { //這是用javah風格的代碼了,推遲解析
  6.             LOGD("No JNI_OnLoad found in %s %p, skipping init",
  7.                 pathName, classLoader);
  8.         } else {
  9.             /*
  10.              * Call JNI_OnLoad.  We have to override the current class
  11.              * loader, which will always be "null" since the stuff at the
  12.              * top of the stack is around Runtime.loadLibrary().  (See
  13.              * the comments in the JNI FindClass function.)
  14.              */
  15.             OnLoadFunc func = (OnLoadFunc)vonLoad;
  16.             Object* prevOverride = self->classLoaderOverride;

  17.             self->classLoaderOverride = classLoader;
  18.             oldStatus = dvmChangeStatus(self, THREAD_NATIVE);
  19.             if (gDvm.verboseJni) {
  20.                 LOGI("[Calling JNI_OnLoad for \"%s\"]", pathName);
  21.             }
  22.             version = (*func)(gDvmJni.jniVm, NULL); //調用JNI_OnLoad,並獲取返回的版本信息
  23.             dvmChangeStatus(self, oldStatus);
  24.             self->classLoaderOverride = prevOverride;

  25.             if (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 &&
  26.                 version != JNI_VERSION_1_6) //對版本進行判斷,這是爲什麼要返回正確版本的原因
  27.             {
  28.                 LOGW("JNI_OnLoad returned bad version (%d) in %s %p",
  29.                     version, pathName, classLoader);
  30.                 /*
  31.                  * It's unwise to call dlclose() here, but we can mark it
  32.                  * as bad and ensure that future load attempts will fail.
  33.                  *
  34.                  * We don't know how far JNI_OnLoad got, so there could
  35.                  * be some partially-initialized stuff accessible through
  36.                  * newly-registered native method calls.  We could try to
  37.                  * unregister them, but that doesn't seem worthwhile.
  38.                  */
  39.                 result = false;
  40.             } else {
  41.                 if (gDvm.verboseJni) {
  42.                     LOGI("[Returned from JNI_OnLoad for \"%s\"]", pathName);
  43.                 }
  44.             }

  45. 上面的代碼說明,JNI_OnLoad是一種更加靈活,而且處理及時的機制。

  46. 用javah風格的代碼,則推遲解析,直到需要調用的時候纔會解析。這樣的函數,是dvmResolveNativeMethod(dalvik/vm/Native.cpp)

  47. dvmResolveNativeMethod
  48. dvmResolveNativeMethod是在一種延遲解析機制,它的代碼是

  49. void dvmResolveNativeMethod(const u4* args, JValue* pResult,
  50.     const Method* method, Thread* self)
  51. {
  52.     ClassObject* clazz = method->clazz;

  53.     /*
  54.      * If this is a static method, it could be called before the class
  55.      * has been initialized.
  56.      */
  57.     if (dvmIsStaticMethod(method)) {
  58.         if (!dvmIsClassInitialized(clazz) && !dvmInitClass(clazz)) {
  59.             assert(dvmCheckException(dvmThreadSelf()));
  60.             return;
  61.         }
  62.     } else {
  63.         assert(dvmIsClassInitialized(clazz) ||
  64.                dvmIsClassInitializing(clazz));
  65.     }

  66.     /* start with our internal-native methods */
  67.     DalvikNativeFunc infunc = dvmLookupInternalNativeMethod(method);
  68.     if (infunc != NULL) {
  69.         /* resolution always gets the same answer, so no race here */
  70.         IF_LOGVV() {
  71.             char* desc = dexProtoCopyMethodDescriptor(&method->prototype);
  72.             LOGVV("+++ resolved native %s.%s %s, invoking",
  73.                 clazz->descriptor, method->name, desc);
  74.             free(desc);
  75.         }
  76.         if (dvmIsSynchronizedMethod(method)) {
  77.             LOGE("ERROR: internal-native can't be declared 'synchronized'");
  78.             LOGE("Failing on %s.%s", method->clazz->descriptor, method->name);
  79.             dvmAbort();     // harsh, but this is VM-internal problem
  80.         }
  81.         DalvikBridgeFunc dfunc = (DalvikBridgeFunc) infunc;
  82.         dvmSetNativeFunc((Method*) method, dfunc, NULL);
  83.         dfunc(args, pResult, method, self);
  84.         return;
  85.     }

  86.     /* now scan any DLLs we have loaded for JNI signatures */
  87.     void* func = lookupSharedLibMethod(method); //注意到,這裏,是獲取地址的地方
  88.     if (func != NULL) {
  89.         /* found it, point it at the JNI bridge and then call it */
  90.         dvmUseJNIBridge((Method*) method, func);
  91.         (*method->nativeFunc)(args, pResult, method, self);
  92.         return;
  93.     }

  94.     IF_LOGW() {
  95.         char* desc = dexProtoCopyMethodDescriptor(&method->prototype);
  96.         LOGW("No implementation found for native %s.%s %s",
  97.             clazz->descriptor, method->name, desc);
  98.         free(desc);
  99.     }

  100.     dvmThrowUnsatisfiedLinkError(method->name);
  101. }
複製代碼
lookupSharedLibMethod函數會調用到函數findMethodInLib,當然,不是直接調用,有興趣的可以參考具體源碼。

findMethodInLib是實現解析的:

static int findMethodInLib(void* vlib, void* vmethod)
{
    const SharedLib* pLib = (const SharedLib*) vlib;
    const Method* meth = (const Method*) vmethod;
    char* preMangleCM = NULL;
    char* mangleCM = NULL;
    char* mangleSig = NULL;
    char* mangleCMSig = NULL;
    void* func = NULL;
    int len;

    if (meth->clazz->classLoader != pLib->classLoader) {
        LOGV("+++ not scanning '%s' for '%s' (wrong CL)",
            pLib->pathName, meth->name);
        return 0;
    } else
        LOGV("+++ scanning '%s' for '%s'", pLib->pathName, meth->name);

    /*
     * First, we try it without the signature.
     */
    preMangleCM =
        createJniNameString(meth->clazz->descriptor, meth->name, &len);
    if (preMangleCM == NULL)
        goto bail;

    mangleCM = mangleString(preMangleCM, len); //這裏,把java的native方法的名字進行轉換,生成和javah一致的名字
    if (mangleCM == NULL)
        goto bail;

    LOGV("+++ calling dlsym(%s)", mangleCM);
    func = dlsym(pLib->handle, mangleCM);
    if (func == NULL) {
        mangleSig =
            createMangledSignature(&meth->prototype);
        if (mangleSig == NULL)
            goto bail;

        mangleCMSig = (char*) malloc(strlen(mangleCM) + strlen(mangleSig) +3);
        if (mangleCMSig == NULL)
            goto bail;

        sprintf(mangleCMSig, "%s__%s", mangleCM, mangleSig);

        LOGV("+++ calling dlsym(%s)", mangleCMSig);
        func = dlsym(pLib->handle, mangleCMSig); //dlsym清晰的表明,這裏纔是獲取符號的地方。
        if (func != NULL) {
            LOGV("Found '%s' with dlsym", mangleCMSig);
        }
    } else {
        LOGV("Found '%s' with dlsym", mangleCM);
    }

bail:
    free(preMangleCM);
    free(mangleCM);
    free(mangleSig);
    free(mangleCMSig);
    return (int) func;
}

實際上,無論是那種方式,從vm的代碼中,都可以看出,這些符號可以放在任意的動態庫中,只要確保他們調用了System.loadLibrary即可。

JNI_OnLoad函數,可以通過registerNatives,在任意時刻替換。

VM把native函數指針通過JNI Bridge,放到一個Method結構中,這個Method結構,最終會放在struct DvmGlobals gDvm;這個全局變量中。

由於是普通的全局變量,在java獨立進程中保存,一旦該全局變量被修改,linux的copy-on-write機制啓動,就會形成一個該進行獨有的一個gDvm變量,從而和其他進行區分開。



利用JNI_OnLoad替換WebCore模塊

在Android的WebViewCore類裏,靜態加載了
  1. static {
  2.         // Load libwebcore and libchromium_net during static initialization.
  3.         // This happens in the zygote process so they will be shared read-only
  4.         // across all app processes.
  5.         try {
  6.             System.loadLibrary("webcore");
  7.             System.loadLibrary("chromium_net");
  8.         } catch (UnsatisfiedLinkError e) {
  9.             Log.e(LOGTAG, "Unable to load native support libraries.");
  10.         }
  11.     }
複製代碼
注意到紅字部分的說明,Android通過zygote進程,來孵化每個新啓動的進程。

Android爲了加快啓動速度,把一些重要的類都放在了preloaded-classes中,這個列表,可以在Android源碼的frameworks/base/preloaded-classes中找到,

也可以在frameworks.jar包中找到,就在最上層。

而webkit相關的類,也在這個proloaded-classes的列表中。它意味着,在android系統啓動時,這些類就都會被加載到系統中。

但是,通過JNI_OnLoad機制,在瀏覽器的主Activiy中,只要加入
  1. static {

  2.   System.loadLibrary("mxwebcore");

  3. }
複製代碼
VM即可實現用新的方法來替換老的方法。

當然,這是僅對當前進程有效,不影響其他進程。
發佈了145 篇原創文章 · 獲贊 92 · 訪問量 91萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章