Android的JNI_OnLoad简介与应用

一、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\n");
}

/*
* 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\n");
}

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

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

....

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\n");
}

//定义批量注册的数组,是注册的关键部分
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\n", kClassName);
        return -1;
    }
   
    /* 
     * 这里就是关键了,把本地函数和一个java类方法关联起来。
       不管之前是否关联过,一律把之前的替换掉!
     */
    if((*env)->RegisterNatives(env, clazz,gMethods, sizeof(gMethods)/sizeof(gMethods[0]))!= JNI_OK) 
    {
        printf("register native method failed!\n");
        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即可实现用新的方法来替换老的方法。
当然,这是仅对当前进程有效,不影响其他进程。

你可能感兴趣的:(Android的JNI_OnLoad简介与应用)