Java JNI_OnLoad的妙用

原文链接:http://liview.cn/discuz/forum.php?mod=viewthread&tid=5


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
  2. #include

  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
  1. #include

  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
  2. #include

  3. #include //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即可实现用新的方法来替换老的方法。

当然,这是仅对当前进程有效,不影响其他进程。

你可能感兴趣的:(Android)