• (OK) Android的JNI_OnLoad简介与应用



    http://blog.csdn.net/fireroll/article/details/50102009


    一、JNI_OnLoad简介 

    Java JNI有两种方法,一种是通过javah,获取一组带签名函数,然后实现这些函数。
    这种方法很常用,也是官方推荐的方法。
    还有一种就是JNI_OnLoad方法。

    当Android的VM(Virtual Machine)执行到C组件(即*so档)里的System.loadLibrary()函数时,
    首先会去执行C组件里的JNI_OnLoad()函数。
    它的用途有二: 
    . 告诉VM此C组件使用那一个JNI版本。
      如果你的*.so档没有提供JNI_OnLoad()函数,VM会默认该*.so档是使用最老的JNI 1.1版本。
      由于新版的JNI做了许多扩充,如果需要使用JNI的新版功能,
      例如JNI 1.4的java.nio.ByteBuffer,就必须藉由JNI_OnLoad()函数来告知VM。

    . 由于VM执行到System.loadLibrary()函数时,就会立即先呼叫JNI_OnLoad(),
      所以C组件的开发者可以藉由JNI_OnLoad()来进行C组件内的初期值之设定(Initialization) 。

    其实Android中的so文件就像是Windows下的DLL一样,JNI_OnLoad和JNI_OnUnLoad函数
    就像是DLL中的PROCESS ATTATCH和DEATTATCH的过程一样,可以同样做一些初始化和反初始化的动作。

    二、Android系统加载JNI Lib的方式

    1. Android系统加载JNI Lib的方式

    Android系统加载JNI Lib的方式有如下两种:
    1) 通过JNI_OnLoad
    2) 如果JNI Lib没有定义JNI_OnLoad,则dvm调用dvmResolveNativeMethod进行动态解析

    2. JNI_OnLoad方法

    System.loadLibrary调用流程如下所示:
    System.loadLibrary->
       Runtime.loadLibrary->(Java)
         nativeLoad->(C: java_lang_Runtime.cpp)
           Dalvik_java_lang_Runtime_nativeLoad->
              dvmLoadNativeCode-> (dalvik/vm/Native.cpp)
                  1) dlopen(pathName, RTLD_LAZY) (把.so mmap到进程空间,并把func等相关信息填充到soinfo中)
                  2) dlsym(handle, "JNI_OnLoad")
                  3) JNI_OnLoad->
                          RegisterNatives->
                             dvmRegisterJNIMethod(ClassObject* clazz, const char* methodName,
                                                    const char* signature, void* fnPtr)->
                                dvmUseJNIBridge(method, fnPtr)->  (method->nativeFunc = func)

    JNI函数在进程空间中的起始地址被保存在ClassObject->directMethods中。
    struct ClassObject : Object {  
        /* static, private, and <init> methods */  
        int             directMethodCount;  
        Method*         directMethods;  
      
        /* virtual methods defined in this class; invoked through vtable */  
        int             virtualMethodCount;  
        Method*         virtualMethods;  
    }  
    此ClassObject通过gDvm.jniGlobalRefTable或gDvm.jniWeakGlobalRefLock获取。

    3. dvmResolveNativeMethod延迟解析机制

    如果JNI Lib中没有JNI_OnLoad,即在执行System.loadLibrary时,
    无法把此JNI Lib实现的函数在进程中的地址增加到ClassObject->directMethods。
    则直到需要调用的时候才会解析这些javah风格的函数 。
    这样的函数dvmResolveNativeMethod(dalvik/vm/Native.cpp)来进行解析,
    其执行流程如下所示:
    void dvmResolveNativeMethod(const u4* args, JValue* pResult,
              const Method* method, Thread* self)  --> (Resolve a native method and invoke it.)
          1) void* func = lookupSharedLibMethod(method)(根据signature在所有已经打开的.so中寻找此函数实现)
                  dvmHashForeach(gDvm.nativeLibs, findMethodInLib,(void*) method)->
                       findMethodInLib(void* vlib, void* vmethod)->
                          dlsym(pLib->handle, mangleCM)

         2) dvmUseJNIBridge((Method*) method, func);
         3) (*method->nativeFunc)(args, pResult, method, self);  (调用执行)

    三、应用实例

    1. 成功调用JNI的实例

    调用的jdk为android 2.1源码下的 jdk1.5.0_22的文件夹

    1.1 静态方式:

    1,首先,在android根目录建立test 目录,在test目录下再建立test目录,进入
    2,进入test目录后vim HelloWorld.java

    //HelloWorld.java:
    package test;
    public class HelloWorld {
        public static void main(String[] args){
            System.loadLibrary("HelloWorld");
            printHello();
        }
        public static native final void printHello();
    }

    3,退出test目录,键入命令:
       $ ../jdk1.5.0_22/bin/javac test/HelloWorld.java
       test目录下将生成HelloWorld.class
    4,键入:
       $ ../jdk1.5.0_22/bin/javah -o test/Hello.h test.HelloWorld
       test目录下将生成Hello.h
    5,在test目录下创建HelloWorld.cpp文件

    //HelloWorld.cpp:
    #include "Hello.h"
    #include <cstdio>

    Void Java_test_HelloWorld_printHello(JNIEnv *, jclass) {
        printf("helloworld");
    }

    6,退出test目录,键入:
      $ g++ test/HelloWorld.cpp -I ../jdk1.5.0_22/include/ -I ../jdk1.5.0_22/include/linux/ -fPIC -shared -o test/libHelloWorld.so
     test目录下将会生成libHelloWorld.so
    7,运行
      $ ../jdk1.5.0_22/bin/java test.HelloWorld 
      屏幕上会打印出helloworld.
     
    出错情况:
    1,如果g++ test/HelloWorld.cpp -I ../jdk1.5.0_22/include/ -fPIC -shared -o test/libHelloWorld.so 
       虽然jni.h在../jdk1.5.0_22/include/,但是如果不加上这边的目录-I ../jdk1.5.0_22/include/linux/的话,
       还是会报一大堆错误的。
    2,如果运行的时候../jdk1.5.0_22/bin/java test.HelloWorld有
    java.lang.UnsatisfiedLinkError: no HelloWorld in library path
             at java.lang.Runtime.loadLibrary(Runtime.java)
             at java.lang.System.loadLibrary(System.java)
             at HelloWorld.main(HelloWorld.java)
    类似的错误,那么就是因为LD_LIBRARY_PATH没有设置正确,用
    LD_LIBRARY_PATH=test
    export LD_LIBRARY_PATH
    在运行一下就可以了。
    参考自:
    http://java.sun.com/docs/books/jni/html/start.html

    1.2 动态方式:

    关于动态方式,java上层的调用和静态方式是相同的,关键是native层的调用有所不同,主要关键在于三个地方:
    1,  定义调用的JNINativeMethod
    2,  定义调用挂钩的函数
    3,  实现JNI_OnLoad函数

    JNI_OnLoad是java jni技术的一个实现,每次java层加载System.loadLibrary之后,
    自动会查找改库一个叫JNI_OnLoad的函数,动态注册的时候,cpp可以通过实现JNI_OnLoad而完成jni的动态注册。
     
    1,建立dynamic文件夹,进入,把静态连接的HelloWorld.java拷贝进来,并且修改package test;为package dynamic;
    2,建立HelloWorld.cpp
    #include "jni.h"
    #include <cstdio>

    // java转到native层的对应函数
    static int android_print(JNIEnv * env, jclass clazz){
        printf("helloworld");
    }

    // 结构体,分别是java层的函数名称,签名,对应的函数指针
    static JNINativeMethod gMethods[] ={
        {"printHello", "()V", (void*)android_print},
    };

    // JNI_OnLoad函数实现
    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 -1;
        }

        /*
         * 如果要注册,只需要两步,
         *    首先FindClass,
         *    然后RegisterNatives
         */
        char className[20] = {"dynamic/HelloWorld"};

        jclass clazz = (env)->FindClass( (const char*)className);
        if((env)->RegisterNatives(clazz, gMethods, 1)< 0) {
            return -1;
        }

        //一定要返回版本号,否则会出错。
        result = JNI_VERSION_1_4;
        return result;
    }

    然后编译一下:
    编译java
    $ ../jdk1.5.0_22/bin/javac dynamic/HelloWorld.java

    编译cpp生成.so
    $ g++ dynamic/HelloWorld.cpp -I ../jdk1.5.0_22/include/ -I ../jdk1.5.0_22/include/linux/ -fPIC -shared -o dynamic /libHelloWorld.so
    dynamic目录下将会生成libHelloWorld.so

    设置LD_LIBRARY_PATH环境变脸
    运行Java
    $ ../jdk1.5.0_22/bin/java dynamic.HelloWorld
    屏幕上会打印出helloworld

    2. 实现动态的函数替换

    JNI_OnLoad可以和JNIEnv的registerNatives函数结合起来,实现动态的函数替换。
    下面用一个简单的例子来说明

    2.1 java类声明
    创建目录mj/jnitest,并新建两个文件:
    . MyObject.java
    . JniTest.java

    // MyObject.java
    package mj.jnitest;

    class MyObject {  
        static {   
            System.loadLibrary("jni");  //这是加载使用javah规定风格实现的库
        }

        //下面定义两个native函数
        public native void func1();
        public native void func2();
    }

    //JniTest.java
    package mj.jnitest;

    class JniTest {
        // static {  //这是一种静态的加载方式,可以完全工作;但是下面我们要用更灵活的方式进行
              //  System.loadLibrary("jni2");
        // }

        public static void main(String[] args)  {   
            MyObject obj = new MyObject();   

            //在fun2函数替换之前,先进行一次调用,会调研jni1中的函数
            obj.func1();   
            obj.func2();   

            //用JNI_OnLoad进行主动注册
            System.loadLibrary("jni2");   
            obj.func1();   
            obj.func2(); //func2已经被jni2中的函数替换
        }
    };

    在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中的了。

    2.2 ni1和jni2的源代码,
    // jni1的源代码mj_jnitest_MyObject.c
    #include <stdio.h>
    #include <stdlib.h>
    #include "mj_jnitest_MyObject.h"

    /*
    * Class:     mj_jnitest_MyObject
    * Method:    func1
    * Signature: ()V
    */
    JNIEXPORT void JNICALL Java_mj_jnitest_MyObject_func1
      (JNIEnv *env, jobject jobj)
    {
        printf("--- func1 called in version 1 ");
    }

    /*
    * Class:     mj_jnitest_MyObject
    * Method:    func2
    * Signature: ()V
    */
    JNIEXPORT void JNICALL Java_mj_jnitest_MyObject_func2
      (JNIEnv *env, jobject jobj)
    {
        printf("--- func2 called in version 1 ");
    }

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

    static void JNICALL func2(JNIEnv *env, jobject jobj)
    {
        printf("--- func2 called in version 2 ");
    }

    ....

    2.3 JNI_OnLoad的使用方法

    先看一下jni2.c的完整源代码,并注意注释
    #include <stdio.h>
    #include <stdlib.h>

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

    //函数名字可以随便取,不过参数一定要和javah生成的函数的参数一致,包括返回值
    static void JNICALL func2  (JNIEnv *env, jobject jobj)
    {


        printf("--- func2 called in version 2 ");
    }

    //定义批量注册的数组,是注册的关键部分
    static const JNINativeMethod gMethods[] = { 
        {"func2",        /* func2是在java中声明的native函数名 */
         "()V",          /* "()V"是函数的签名,可以通过javah获取。*/
         (void*)func2
        } ,
    };

    //这是JNI_OnLoad的声明,必须按照这样的方式声明
    JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void *reserved)
    {
        JNIEnv* env = NULL; //注册时在JNIEnv中实现的,所以必须首先获取它
        jint result = -1;

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

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

        /* 
         * 这里可以找到要注册的类,前提是这个类已经加载到java虚拟机中。 
         * 这里说明,动态库和有native方法的类之间,没有任何对应关系。
         */
        clazz = (*env)->FindClass(env, kClassName); 
        if(clazz == NULL) {
            printf("cannot get class:%s ", kClassName);
            return -1;
        }
       
        /* 
         * 这里就是关键了,把本地函数和一个java类方法关联起来。
           不管之前是否关联过,一律把之前的替换掉!
         */
        if((*env)->RegisterNatives(env, clazz,gMethods, sizeof(gMethods)/sizeof(gMethods[0]))!= JNI_OK) 
        {
            printf("register native method failed! ");
            return -1;
        }

        //这里很重要,必须返回版本,否则加载会失败。
        return JNI_VERSION_1_4; 
    }

    对他进行编译后,得到一个libjni2.so。

    2.4 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]))

    2.5 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,可以找到以下代码
    bool result = true;
            void* vonLoad;
            int version;

            vonLoad = dlsym(handle, "JNI_OnLoad"); //获取JNI_OnLoad的地址
            if (vonLoad == NULL) { //这是用javah风格的代码了,推迟解析
                LOGD("No JNI_OnLoad found in %s %p, skipping init",
                    pathName, classLoader);
            } else {
                /*
                 * Call JNI_OnLoad.  We have to override the current class
                 * loader, which will always be "null" since the stuff at the
                 * top of the stack is around Runtime.loadLibrary().  (See
                 * the comments in the JNI FindClass function.)
                 */
                OnLoadFunc func = (OnLoadFunc)vonLoad;
                Object* prevOverride = self->classLoaderOverride;

                self->classLoaderOverride = classLoader;
                oldStatus = dvmChangeStatus(self, THREAD_NATIVE);
                if (gDvm.verboseJni) {
                    LOGI("[Calling JNI_OnLoad for "%s"]", pathName);
                }
                version = (*func)(gDvmJni.jniVm, NULL); //调用JNI_OnLoad,并获取返回的版本信息
                dvmChangeStatus(self, oldStatus);
                self->classLoaderOverride = prevOverride;

                if (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 &&
                    version != JNI_VERSION_1_6) //对版本进行判断,这是为什么要返回正确版本的原因
                {
                    LOGW("JNI_OnLoad returned bad version (%d) in %s %p",
                        version, pathName, classLoader);
                    /*
                     * It's unwise to call dlclose() here, but we can mark it
                     * as bad and ensure that future load attempts will fail.
                     *
                     * We don't know how far JNI_OnLoad got, so there could
                     * be some partially-initialized stuff accessible through
                     * newly-registered native method calls.  We could try to
                     * unregister them, but that doesn't seem worthwhile.
                     */
                    result = false;
                } else {
                    if (gDvm.verboseJni) {
                        LOGI("[Returned from JNI_OnLoad for "%s"]", pathName);
                    }
                }

    上面的代码说明,JNI_OnLoad是一种更加灵活,而且处理及时的机制。

    用javah风格的代码,则推迟解析,直到需要调用的时候才会解析。
    这样的函数,是dvmResolveNativeMethod(dalvik/vm/Native.cpp)

    dvmResolveNativeMethod
    dvmResolveNativeMethod是在一种延迟解析机制,它的代码是
    void dvmResolveNativeMethod(const u4* args, JValue* pResult,
        const Method* method, Thread* self)
    {
        ClassObject* clazz = method->clazz;

        /*
         * If this is a static method, it could be called before the class
         * has been initialized.
         */
        if (dvmIsStaticMethod(method)) {
            if (!dvmIsClassInitialized(clazz) && !dvmInitClass(clazz)) {
                assert(dvmCheckException(dvmThreadSelf()));
                return;
            }
        } else {
            assert(dvmIsClassInitialized(clazz) ||
                   dvmIsClassInitializing(clazz));
        }

        /* start with our internal-native methods */
        DalvikNativeFunc infunc = dvmLookupInternalNativeMethod(method);
        if (infunc != NULL) {
            /* resolution always gets the same answer, so no race here */
            IF_LOGVV() {
                char* desc = dexProtoCopyMethodDescriptor(&method->prototype);
                LOGVV("+++ resolved native %s.%s %s, invoking",
                    clazz->descriptor, method->name, desc);
                free(desc);
            }
            if (dvmIsSynchronizedMethod(method)) {
                LOGE("ERROR: internal-native can't be declared 'synchronized'");
                LOGE("Failing on %s.%s", method->clazz->descriptor, method->name);
                dvmAbort();     // harsh, but this is VM-internal problem
            }
            DalvikBridgeFunc dfunc = (DalvikBridgeFunc) infunc;
            dvmSetNativeFunc((Method*) method, dfunc, NULL);
            dfunc(args, pResult, method, self);
            return;
        }


        /* now scan any DLLs we have loaded for JNI signatures */
        void* func = lookupSharedLibMethod(method); //注意到,这里,是获取地址的地方
        if (func != NULL) {
            /* found it, point it at the JNI bridge and then call it */
            dvmUseJNIBridge((Method*) method, func);
            (*method->nativeFunc)(args, pResult, method, self);
            return;
        }


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


        dvmThrowUnsatisfiedLinkError(method->name);
    }


    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变量,从而和其他进行区分开。


    2.6 利用JNI_OnLoad替换WebCore模块

    在Android的WebViewCore类里,静态加载了
    static {
            // Load libwebcore and libchromium_net during static initialization.
            // This happens in the zygote process so they will be shared read-only
            // across all app processes.
            try {
                System.loadLibrary("webcore");
                System.loadLibrary("chromium_net");
            } catch (UnsatisfiedLinkError e) {
                Log.e(LOGTAG, "Unable to load native support libraries.");
            }
        }


    注意到红字部分的说明,Android通过zygote进程,来孵化每个新启动的进程。


    Android为了加快启动速度,把一些重要的类都放在了preloaded-classes中,
    这个列表,可以在Android源码的frameworks/base/preloaded-classes中找到,


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


    而webkit相关的类,也在这个proloaded-classes的列表中。
    它意味着,在android系统启动时,这些类就都会被加载到系统中。


    但是,通过JNI_OnLoad机制,在浏览器的主Activiy中,只要加入
    static {
      System.loadLibrary("mxwebcore");
    }

    VM即可实现用新的方法来替换老的方法。
    当然,这是仅对当前进程有效,不影响其他进程。


  • 相关阅读:
    P2325 [SCOI2005]王室联邦
    P2709 小B的询问
    P4867 Gty的二逼妹子序列
    P4396 [AHOI2013]作业
    CF617E XOR and Favorite Number
    P4462 [CQOI2018]异或序列
    p4434 [COCI2017-2018#2] ​​Usmjeri
    LOJ 117 有源汇有上下界最小流
    P4137 Rmq Problem / mex
    LOJ 116 有源汇有上下界最大流
  • 原文地址:https://www.cnblogs.com/ztguang/p/12645153.html
Copyright © 2020-2023  润新知