zoukankan      html  css  js  c++  java
  • Java中JIN机制及System.loadLibrary() 的执行过程

    Android平台Native开发与JNI机制详解

    http://mysuperbaby.iteye.com/blog/915425

    个人认为下面这篇转载的文章写的很清晰很不错. 注意Android平台上的JNI机制使用包括Java代码中调用Native模块以及Native代码中调用Java模块.

    http://www.ophonesdn.com/article/show/263 (misybing:很遗憾该站已经挂掉)

    众所周知,OPhone平台上的应用开发主要基于Java语言,但平台完全支持且提供了一定的Native开发能力(主要是C/C++),使得开发者可以借助JNI更深入的实现创意。本文主要介绍OPhone平台的JNI机制和Native模块开发与发布的方法。

    JNI简介
    Java Native Interface(JNI)是Java提供的一个很重要的特性。它使得用诸如C/C++等语言编写的代码可以与运行于Java虚拟机(JVM)中的 Java代码集成。有些时候,Java并不能满足你的全部开发需求,比如你希望提高某些关键模块的效率,或者你必须使用某个以C/C++等Native语 言编写的程序库;此时,JNI就能满足你在Java代码中访问这些Native模块的需求。JNI的出现使得开发者既可以利用Java语言跨平台、类库丰 富、开发便捷等特点,又可以利用Native语言的高效。
     
     
    图1 JNI与JVM的关系
     
     
    实际上,JNI是JVM实现中的一部分,因此Native语言和Java代码都运行在JVM的宿主环境(Host Environment),正如图1所示。此外,JNI是一个双向的接口:开发者不仅可以通过JNI在Java代码中访问Native模块,还可以在 Native代码中嵌入一个JVM,并通过JNI访问运行于其中的Java模块。可见,JNI担任了一个桥梁的角色,它将JVM与Native模块联系起 来,从而实现了Java代码与Native代码的互访。在OPhone上使用Java虚拟机是为嵌入式设备特别优化的Dalvik虚拟机。每启动一个应 用,系统会建立一个新的进程运行一个Dalvik虚拟机,因此各应用实际上是运行在各自的VM中的。Dalvik VM对JNI的规范支持的较全面,对于从JDK 1.2到JDK 1.6补充的增强功能也基本都能支持。
    开发者在使用JNI之前需要充分了解其优缺点,以便合理选择技术方案实现目标。JNI的优点前面已经讲过,这里不再重复,其缺点也 是显而易见的:由于Native模块的使用,Java代码会丧失其原有的跨平台性和类型安全等特性。此外,在JNI应用中,Java代码与Native代 码运行于同一个进程空间内;对于跨进程甚至跨宿主环境的Java与Native间通信的需求,可以考虑采用socket、Web Service等IPC通信机制来实现。
     
    在OPhone开发中使用JNI
    正如我们在上一节所述,JNI是一个双向的接口,所以交互的类型可以分为在Java代码中调用Native模块和在Native代码中调用Java模块两种。下面,我们就使用一个Hello-JNI的示例来分别对这两种交互方式的开发要点加以说明。
     
    Java调用Native模块
    Hello-JNI这个示例的结构很简单:首先我们使用Eclipse新建一个OPhone应用的Java工程,并添加一个 com.example.hellojni.HelloJni的类。这个类实际上是一个Activity,稍后我们会创建一个TextView,并显示一 些文字在上面。
    要在Java代码中使用Native模块,必须先对Native函数进行声明。在我们的例子中,打开HelloJni.java文件,可以看到如下的声明:
     
    1. /* A native method that is implemented by the  
    2.    * 'hello-jni' native library, which is packaged  
    3.    * with this application.  
    4.    */   
    5.   public   native  String  stringFromJNI();  
    Java代码  收藏代码
    1. /* A native method that is implemented by the 
    2.    * 'hello-jni' native library, which is packaged 
    3.    * with this application. 
    4.    */  
    5.   public native String  stringFromJNI();  
      
    从上述声明中我们可以知道,这个stringFromJNI()函数就是要在Java代码中调用的Native函数。接下来我们要创建一个hello-jni.c的C文件,内容很简单,只有如下一个函数:
     
     
    1. #include <string.h>  
    2. #include <jni.h>  
    3. jstring  
    4. Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,  
    5.                                                  jobject thiz ) {  
    6.         return  (*env)->NewStringUTF(env,  "Hello from JNI !" );  
    7. }  
    Java代码  收藏代码
    1. #include <string.h>  
    2. #include <jni.h>  
    3. jstring  
    4. Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,  
    5.                                                  jobject thiz ) {  
    6.         return (*env)->NewStringUTF(env, "Hello from JNI !");  
    7. }  
    从函数名可以看出,这个Native函数对应的正是我们在com.example.hellojni.HelloJni这个中声明的Native函数String stringFromJNI()的具体实现。
     
    从上面Native函数的命名上我们可以了解到JNI函数的命名规则: Java代码中的函数声明需要添加native 关键 字;Native的对应函数名要以“Java_”开头,后面依次跟上Java的“package名”、“class名”、“函数名”,中间以下划线“_” 分割,在package名中的“.”也要改为“_”。此外,关于函数的参数和返回值也有相应的规则。对于Java中的基本类型如intdoublechar 等,在Native端都有相对应的类型来表示,如jintjdoublejchar 等;其他的对象类型则统统由jobject 来表示(String 是个例外,由于其使用广泛,故在Native代码中有jstring 这个类型来表示,正如在上例中返回值String 对应到Native代码中的返回值jstring )。而对于Java中的数组,在Native中由jarray 对应,具体到基本类型和一般对象类型的数组则有jintArray 等和jobjectArray 分别对应(String 数组在这里没有例外,同样用jobjectArray 表示)。还有一点需要注意的是,在JNI的Native函数中,其前两个参数JNIEnv *和jobject 是必需的——前者是一个JNIEnv 结构体的指针,这个结构体中定义了很多JNI的接口函数指针,使开发者可以使用JNI所定义的接口功能;后者指代的是调用这个JNI函数的Java对象,有点类似于C++中的this 指针。在上述两个参数之后,还需要根据Java端的函数声明依次对应添加参数。在上例中,Java中声明的JNI函数没有参数,则Native的对应函数只有类型为JNIEnv *和jobject 的两个参数。
     
    当然,要使用JNI函数,还需要先加载Native代码编译出来的动态库文件(在Windows上是.dll,在Linux上则为.so)。这个动作是通过如下语句完成的:
     
    1. static  {  
    2.     System.loadLibrary("hello-jni" );  
    3. }  
    Java代码  收藏代码
    1. static {  
    2.     System.loadLibrary("hello-jni");  
    3. }  
     
    注意这里调用的共享库名遵循Linux对库文件的命名惯例,因为OPhone的核心实际上是Linux系统——上例中,实际加载的库文件应为 “libhello-jni.so”,在引用时遵循命名惯例,不带“lib”前缀和“.so”的扩展名。对于没有按照上述惯例命名的Native库,在加 载时仍需要写成完整的文件名。
     
    JNI函数的使用方法和普通Java函数一样。在本例中,调用代码如下:
     
    1. TextView tv =  new  TextView( this );  
    2. tv.setText( stringFromJNI() );  
    3. setContentView(tv);  
    Java代码  收藏代码
    1. TextView tv = new TextView(this);  
    2. tv.setText( stringFromJNI() );  
    3. setContentView(tv);  
     
    就可以在TextView中显示出来自于Native函数的字符串。怎么样,是不是很简单呢?
     
    Native调用Java模块
    从OPhone的系统架构来看,JVM和Native系统库位于内核之上,构成OPhone Runtime;更多的系统功能则是通过在其上的Application Framework以Java API的形式提供的。因此,如果希望在Native库中调用某些系统功能,就需要通过JNI来访问Application Framework提供的API。
     
    JNI规范定义了一系列在Native代码中访问Java对象及其成员与方法的API。下面我们还是通过示例来具体讲解。首先,新建一个SayHello 的类,代码如下:
     
    1. package  com.example.hellojni;  
    2. public   class  SayHello {  
    3.         public  String sayHelloFromJava(String nativeMsg) {  
    4.                String str = nativeMsg + " But shown in Java!" ;  
    5.                return  str;  
    6.         }  
    7. }  
    Java代码  收藏代码
    1. package com.example.hellojni;  
    2. public class SayHello {  
    3.         public String sayHelloFromJava(String nativeMsg) {  
    4.                String str = nativeMsg + " But shown in Java!";  
    5.                return str;  
    6.         }  
    7. }  
     
    接下来要实现的就是在Native代码中调用这个SayHello 类中的sayHelloFromJava方法。
     
    一般来说,要在Native代码中访问Java对象,有如下几个步骤:
    1.         得到该Java对象的类定义。JNI定义了jclass 这个类型来表示Java的类的定义,并提供了FindClass接口,根据类的完整的包路径即可得到其jclass
    2.         根据jclass 创建相应的对象实体,即jobject 。在Java中,创建一个新对象只需要使用new 关键字即可,但在Native代码中创建一个对象则需要两步:首先通过JNI接口GetMethodID得到该类的构造函数,然后利用NewObject接口构造出该类的一个实例对象。
    3.         访问jobject 中的成员变量或方法。访问对象的方法是先得到方法的Method ID,然后使用Call<Type >Method 接口调用,这里Type对应相应方法的返回值——返回值为基本类型的都有相对应的接口,如CallIntMethod;其他的返回值(包括String) 则为CallObjectMethod。可以看出,创建对象实质上是调用对象的一个特殊方法,即构造函数。访问成员变量的步骤一样:首先 GetFieldID得到成员变量的ID,然后Get/Set<Type >Field读/写变量值。
     
    上面概要介绍了从Native代码中访问Java对象的过程,下面我们结合示例来具体看一下。如下是调用sayHelloFromJava方法的Native代码:
     
    1. jstring helloFromJava( JNIEnv* env ) {  
    2.        jstring str = NULL;  
    3.        jclass clz = (*env)->FindClass(env, "com/example/hellojni/SayHello" );  
    4.        jmethodID ctor = (*env)->GetMethodID(env, clz, "<init>" ,  "()V" );  
    5.        jobject obj = (*env)->NewObject(env, clz, ctor);  
    6.        jmethodID mid = (*env)->GetMethodID(env, clz, "sayHelloFromJava" ,  "(Ljava/lang/String;)Ljava/lang/String;" );  
    7.        if  (mid) {  
    8.               jstring jmsg = (*env)->NewStringUTF(env, "I'm born in native." );  
    9.               str = (*env)->CallObjectMethod(env, obj, mid, jmsg);  
    10.        }  
    11.        return  str;  
    12. }  
    Java代码  收藏代码
    1. jstring helloFromJava( JNIEnv* env ) {  
    2.        jstring str = NULL;  
    3.        jclass clz = (*env)->FindClass(env, "com/example/hellojni/SayHello");  
    4.        jmethodID ctor = (*env)->GetMethodID(env, clz, "<init>", "()V");  
    5.        jobject obj = (*env)->NewObject(env, clz, ctor);  
    6.        jmethodID mid = (*env)->GetMethodID(env, clz, "sayHelloFromJava", "(Ljava/lang/String;)Ljava/lang/String;");  
    7.        if (mid) {  
    8.               jstring jmsg = (*env)->NewStringUTF(env, "I'm born in native.");  
    9.               str = (*env)->CallObjectMethod(env, obj, mid, jmsg);  
    10.        }  
    11.        return str;  
    12. }  
     
     
    可以看到,上述代码和前面讲到的步骤完全相符。这里提一下编程时要注意的要点:1、FindClass要写明Java类的完整包路径,并将 “.”以“/”替换;2、GetMethodID的第三个参数是方法名(对于构造函数一律用“<init>”表示),第四个参数是方法的“签 名”,需要用一个字符串序列表示方法的参数(依声明顺序)和返回值信息。由于篇幅所限,这里不再具体说明如何根据方法的声明构造相应的“签名”,请参考 JNI的相关文档。
     
    关于上面谈到的步骤再补充说明一下:在JNI规范中,如上这种使用NewObject创建的对象实例被称为“Local Reference”,它仅在创建它的Native代码作用域内有效,因此应避免在作用域外使用该实例及任何指向它的指针。如果希望创建的对象实例在作用 域外也能使用,则需要使用NewGlobalRef接口将其提升为“Global Reference”——需要注意的是,当Global Reference不再使用后,需要显式的释放,以便通知JVM进行垃圾收集。
     
    Native模块的编译与发布
     
    通过前面的介绍,我们已经大致了解了在OPhone的应用开发中使用JNI的方法。那么,开发者如何编译出能在OPhone上使用的Native模块呢?编译出的Native模块又如何像APK文件那样分发、安装呢?
     
    Google于2009年6月底发布了Android NDK的第一个版本,为广大开发者提供了编译用于Android应用的Native模块的能力,以及将Native模块随Java应用打包为APK文件, 以便分发和安装的整套解决方案。NDK的全称是Native Development Toolkit,即原生应用开发包。由于OPhone平台也基于Android,因此使用Android NDK编译的原生应用或组件完全可以用于OPhone。需要注意的是,Google声称此次发布的NDK仅兼容于Android 1.5及以后的版本,由于OPhone 1.0平台基于Android 1.5之前的版本,虽然不排除使用该NDK开发的原生应用或组件在OPhone 1.0平台上正常运行的可能性,但建议开发者仅在OPhone 1.5及以上的平台使用。
     
    最新版本的NDK可以在http://developer.android.com/sdk/ndk/index.html 下载。NDK提供了适用于Windows、Linux和MAC OS X的版本,开发者可以根据自己的操作系统下载相应的版本。本文仅使用基于Linux的NDK版本做介绍和演示。
     
    NDK的安装很简单:解压到某个路径下即可,之后可以看到若干目录。其中docs目录中包含了比较详细的文档,可供开发者参考,在NDK根目录 下的README.TXT也对个别重要文档进行了介绍;build目录则包含了用于Android设备的交叉编译器和相关工具,以及一组系统头文件和系统 库,其中包括libc、libm、libz、liblog(用于Android设备log输出)、JNI接口及一个C++标准库的子集(所谓“子集”是指 Android对C++支持有限,如不支持Exception及STL等);apps目录是用于应用开发的目录,out目录则用于编译中间结果的存储。接 下来,我们就用前面的例子简单讲解一下NDK的使用。
     
    进入<ndk>/apps目录,我们可以看到一些示例应用,以hello-jni为例:在hello-jni目录中有一个 Application.mk文件和一个project文件夹,project文件夹中则是一个OPhone Java应用所有的工程文件,其中jni目录就是Native代码放置的位置。这里Application.mk主要用于告诉编译器应用所需要用到的 Native模块有什么,对于一般开发在示例提供的文件的基础上进行修改即可;如果需要了解更多,可参考<ndk>/docs /APPLICATION-MK.txt。接下来,我们将示例文件与代码如图2放置到相应的位置:
     
     
     
    图2 Hello-JNI示例的代码结构
     
    可以看到,和Java应用一样,Native模块也需要使用Android.mk文件设置编译选项和参数,但内容有较大不同。对于Native模块而言,一般需要了解如下几类标签:
     
    1.         LOCAL_MODULE:定义了在整个编译环境中的各个模块, 其名字应当是唯一的。此外,这里设置的模块名称还将作为编译出来的文件名:对于原生可执行文件,文件名即为模块名称;对于静态/动态库文件,文件名为 lib+模块名称。例如hello-jni的模块名称为“hello-jni”,则编译出来的动态库就是libhello-jni.so。
    2.         LOCAL_SRC_FILES:这里要列出所有需要编译的C/C++源文件,以空格或制表符分隔;如需换行,可放置“”符号在行尾,这和GNU Makefile的规则是一致的。
    3.         LOCAL_CFLAGS:定义gcc编译时的CFLAGS参数,与GNU Makefile的规则一致。比如,用-I参数可指定编译所需引用的某个路径下的头文件。
    4.         LOCAL_C_INCLUDES:指定自定义的头文件路径。
    5.         LOCAL_SHARED_LIBRARIES:定义链接时所需要的共享库文件。这里要链接的共享库并不限于NDK编译环境中定义的所有模块。如果需要引用其他的库文件,也可在此处指定。
    6.         LOCAL_STATIC_LIBRARIES:和上个标签类似,指定需要链接的静态库文件。需要注意的是这个选项只有在编译动态库的时候才有意义。
    7.         LOCAL_LDLIBS:定义链接时需要引入的系统库。使用时需要加-l前缀,例如-lz指的是在加载时链接libz这个系统库。libc、libm和libstdc++是编译系统默认会链接的,无需在此标签中指定。
     
    欲了解更多关于标签类型及各类标签的信息,可参考<ndk>/docs/ANDROID-MK.txt文件,其中详细描述了Android.mk中各个标签的含义与用法。如下给出的就是我们的示例所用的Android.mk:
     
     
    1. LOCAL_PATH := $(call my-dir)  
    2. include $(CLEAR_VARS)  
    3. LOCAL_MODULE    :=  hello-jni  
    4. LOCAL_C_INCLUDES :=  $(LOCAL_PATH)/include  
    5. LOCAL_SRC_FILES   :=  src/call_java.c   
    6.                                           src/hello-jni.c   
    7. include $(BUILD_SHARED_LIBRARY)  
    Java代码  收藏代码
    1. LOCAL_PATH := $(call my-dir)  
    2. include $(CLEAR_VARS)  
    3. LOCAL_MODULE    :=  hello-jni  
    4. LOCAL_C_INCLUDES :=  $(LOCAL_PATH)/include  
    5. LOCAL_SRC_FILES   :=  src/call_java.c   
    6.                                           src/hello-jni.c   
    7. include $(BUILD_SHARED_LIBRARY)  
     
     
    写好了代码和Makefile,接下来就是编译了。使用NDK进行编译也很简单:首先从命令行进入<ndk>目录,执 行./build/host-setup.sh,当打印出“Host setup complete.”的文字时,编译环境的设置就完成了。这里开发者需要注意的是,如果使用的Linux发行版是Debian或者Ubuntu,需要通过 在<ndk>目录下执行bash build/host-setup.sh,因为上述两个发行版使用的dash shell与脚本有兼容问题。接下来,输入make APP=hello-jni,稍等片刻即完成编译,如图3所示。从图中可以看到,在编译完成后,NDK会自动将编译出来的共享库拷贝到Java工程的 libs/armeabi目录下。当编译Java工程的时候,相应的共享库会被一同打包到apk文件中。在应用安装时,被打包在libs/armeabi 目录中的共享库会被自动拷贝到/data/data/com.example.HelloJni/lib/目录;当System.loadLibrary 被调用时,系统就可以在上述目录寻找到所需的库文件libhello-jni.so。如果实际的Java工程不在这里,也可以手动在Java工程下创建 libs/armeabi目录,并将编译出来的so库文件拷贝过去。
     
     
    图3 使用NDK编译Hello-JNI
     
     
    最后,将Java工程连带库文件一同编译并在OPhone模拟器中运行,结果如图4所示。
     
    通过上面的介绍,你应该已经对OPhone上的Native开发有了初步了解,或许也已经跃跃欲试了。事实上,尽管Native开发在 OPhone上不具有Java语言的类型安全、兼容性好、易于调试等特性,也无法直接享受平台提供的丰富的API,但JNI还是为我们提供了更多的选择, 使我们可以利用原生应用的优势来做对性能要求高的操作,也可以利用或移植C/C++领域现有的众多功能强大的类库或应用,为开发者提供了充分的施展空间。 这就是OPhone的魅力!
     
     
    图4 Hello-JNI在OPhone模拟器上的运行结果
    参考文献
    [1]      Sheng Liang. Java Native Interface: Programmer's Guide and Specification. http://java.sun.com/docs/books/jni/ .
    [2]      Sun Microsystems. Java Native Interface Specification v1.5. http://java.sun.com/j2se/1.5.0/docs/guide/jni/spec/jniTOC.html .

    System.loadLibrary() 的执行过程

    http://my.oschina.net/wolfcs/blog/129696

    System.loadLibrary()是我们在使用Java的JNI机制时,会用到的一个非常重要的函数,它的作用即是把实现了我们在Java code中声明的native方法的那个libraryload进来,或者load其他什么动态连接库。

    算是处于好奇吧,我们可以看一下这个方法它的实现,即执行流程。(下面分析的那些 code,来自于android 4.2.2 aosp版。)先看一下这个方法的code(在libcore/luni/src/main/java/java/lang/System.java这个 文件中):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
     * Loads and links the library with the specified name. The mapping of the
     * specified library name to the full path for loading the library is
     * implementation-dependent.
     *
     * @param libName
     *            the name of the library to load.
     * @throws UnsatisfiedLinkError
     *             if the library could no<span style="color:#003399;"></span>t be loaded.
     */
    public static void loadLibrary(String libName) {
        Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
    }

    由上面的那段code,可以看到,它的实现非常简单,就只是先调用VMStack.getCallingClassLoader()获取到ClassLoader,然后再把实际要做的事情委托给了Runtime来做而已。接下来我们再看一下Runtime.loadLibrary()的实现(在libcore/luni/src/main/java/java/lang/Runtime.java这个文件中):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    /*
     * Loads and links a library without security checks.
     */
    void loadLibrary(String libraryName, ClassLoader loader) {
        if (loader != null) {
            String filename = loader.findLibrary(libraryName);
            if (filename == null) {
                throw new UnsatisfiedLinkError("Couldn't load " + libraryName
                                               + " from loader " + loader
                                               + ": 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);
    }

    由上面的那段code,我们看到,loadLibrary()可以被看作是一个2步走的过程

    1. 获取到library path。对于这一点,上面的那个函数,依据于所传递的ClassLoader的不同,会有两种不同的方法。如果ClassLoader非空,则会利用ClassLoader的findLibrary()方法来获取library的path。而如果ClassLoader为空,则会首先依据传递进来的library name,获取到library file的name,比如传递“hello”进来,它的library file name,经过System.mapLibraryName(libraryName)将会是“libhello.so”;然后再在一个path list(即上面那段code中的mLibPaths)中查找到这个library file,并最终确定library 的path。
    2. 调用nativeLoad()这个native方法来load library

    这段code,又牵出几个问题,首先,可用的library path都是哪些,这实际上也决定了,我们的so文件放在哪些folder下,才可以被真正load起来?其次,在native层load library的过程,又实际做了什么事情?下面会对这两个问题,一一的作出解答。

    系统的library path

    我们由简单到复杂的来看这个问题。先来看一下,在传入的ClassLoader为空的情况(尽管我们知道,在System.loadLibrary()这个case下不会发生),前面Runtime.loadLibrary()的实现中那个mLibPaths的初始化的过程,在Runtime的构造函数中,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
     * Prevent this class from being instantiated.
     */
    private Runtime(){
        String pathList = System.getProperty("java.library.path", ".");
        String pathSep = System.getProperty("path.separator", ":");
        String fileSep = System.getProperty("file.separator", "/");
     
        mLibPaths = pathList.split(pathSep);
     
        // Add a '/' to the end so we don't have to do the property lookup
        // and concatenation later.
        for (int i = 0; i < mLibPaths.length; i++) {
            if (!mLibPaths[i].endsWith(fileSep)) {
                mLibPaths[i] += fileSep;
            }
        }
    }

    可以看到,那个library path list实际上读取自一个system property。那在android系统中,这个system property的实际内容又是什么呢?dump这些内容出来,就像下面这样:

    1
    2
    3
    05-11 07:51:40.974: V/QRCodeActivity(11081): pathList = /vendor/lib:/system/lib
    05-11 07:51:40.974: V/QRCodeActivity(11081): pathSep = :
    05-11 07:51:40.974: V/QRCodeActivity(11081): fileSep = /


    然后是传入的ClassLoader非空的情况,ClassLoaderfindLibrary()方法的执行过程。首先看一下它的实现(在libcore/luni/src/main/java/java/lang/ClassLoader.java这个文件中)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
     * Returns the absolute path of the native library with the specified name,
     * or {@code null}. If this method returns {@code null} then the virtual
     * machine searches the directories specified by the system property
     * "java.library.path".
     * <p>
     * This implementation always returns {@code null}.
     * </p>
     *
     * @param libName
     *            the name of the library to find.
     * @return the absolute path of the library.
     */
    protected String findLibrary(String libName) {
        return null;
    }

    竟然是一个空函数。那系统中实际运行的ClassLoader就是这个吗?我们可以做一个小小的实验,打印系统中实际运行的ClassLoader的String:

    1
    2
    ClassLoader classLoader = getClassLoader();
    Log.v(TAG, "classLoader = " + classLoader.toString());
    在Galaxy Nexus上执行的结果如下:
    1
    05-11 08:18:57.857: V/QRCodeActivity(11556): classLoader = dalvik.system.PathClassLoader[dexPath=/data/app/com.qrcode.qrcode-1.apk,libraryPath=/data/app-lib/com.qrcode.qrcode-1]
    看到了吧,android系统中的 ClassLoader真正的实现 在dalvik的dalvik.system.PathClassLoader。打开libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java来看 PathClassLoader这个class 的实现,可以看到,就只是简单的继承 BaseDexClassLoader而已,没有任何实际的内容 。接下来我们就来看一下 BaseDexClassLoader中 那个 findLibrary() 真正的实现( 在libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java这个文件中 ):
    1
    2
    3
    4
    @Override
    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }

    这个方法看上去倒挺简单,不用多做解释。然后来看那个pathList的初始化的过程,在BaseDexClassLoader的构造函数里

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /**
     * Constructs an instance.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     * should be written; may be {@code null}
     * @param libraryPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
     
        this.originalPath = dexPath;
        this.originalLibraryPath = libraryPath;
        this.pathList =
            new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    BaseDexClassLoader的构造函数也不用多做解释吧。然后是DexPathList的构造函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    /**
     * Constructs an instance.
     *
     * @param definingContext the context in which any as-yet unresolved
     * classes should be defined
     * @param dexPath list of dex/resource path elements, separated by
     * {@code File.pathSeparator}
     * @param libraryPath list of native library directory path elements,
     * separated by {@code File.pathSeparator}
     * @param optimizedDirectory directory where optimized {@code .dex} files
     * should be found and written to, or {@code null} to use the default
     * system directory for same
     */
    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        if (definingContext == null) {
            throw new NullPointerException("definingContext == null");
        }
     
        if (dexPath == null) {
            throw new NullPointerException("dexPath == null");
        }
     
        if (optimizedDirectory != null) {
            if (!optimizedDirectory.exists())  {
                throw new IllegalArgumentException(
                        "optimizedDirectory doesn't exist: "
                        + optimizedDirectory);
            }
     
            if (!(optimizedDirectory.canRead()
                            && optimizedDirectory.canWrite())) {
                throw new IllegalArgumentException(
                        "optimizedDirectory not readable/writable: "
                        + optimizedDirectory);
            }
        }
     
        this.definingContext = definingContext;
        this.dexElements =
            makeDexElements(splitDexPath(dexPath), optimizedDirectory);
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }

    关于我们的library path的问题,可以只关注最后的那个splitLibraryPath(),这个地方,实际上即是把传进来的libraryPath 又丢给splitLibraryPath来获取library path 的list。可以看一下DexPathList.splitLibraryPath()的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
     * Splits the given library directory path string into elements
     * using the path separator ({@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android, appending on the elements
     * from the system library path, and pruning out any elements that
     * do not refer to existing and readable directories.
     */
    private static File[] splitLibraryPath(String path) {
        /*
         * Native libraries may exist in both the system and
         * application library paths, and we use this search order:
         *
         *   1. this class loader's library path for application
         *      libraries
         *   2. the VM's library path from the system
         *      property for system libraries
         *
         * This order was reversed prior to Gingerbread; see http://b/2933456.
         */
        ArrayList<File> result = splitPaths(
                path, System.getProperty("java.library.path", "."), true);
        return result.toArray(new File[result.size()]);
    }

    这个地方,是在用两个部分的library path list来由splitPaths构造最终的那个path list,一个部分是,传进来的library path,另外一个部分是,像我们前面看到的那个,是system property。然后再来看一下DexPathList.splitPaths()的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
     * Splits the given path strings into file elements using the path
     * separator, combining the results and filtering out elements
     * that don't exist, aren't readable, or aren't either a regular
     * file or a directory (as specified). Either string may be empty
     * or {@code null}, in which case it is ignored. If both strings
     * are empty or {@code null}, or all elements get pruned out, then
     * this returns a zero-element list.
     */
    private static ArrayList<File> splitPaths(String path1, String path2,
            boolean wantDirectories) {
        ArrayList<File> result = new ArrayList<File>();
     
        splitAndAdd(path1, wantDirectories, result);
        splitAndAdd(path2, wantDirectories, result);
        return result;
    }

    总结一下,ClassLoader的那个findLibrary()实际上会在两 个部分的folder中去寻找System.loadLibrary()要load的那个library,一个部分是,构造ClassLoader时,传 进来的那个library path,即是app folder,另外一个部分是system property。在android系统中,查找要load的library,实际上会在如下3个folder中进行:

    1. /vendor/lib
    2. /system/lib
    3. /data/app-lib/com.qrcode.qrcode-1

    上面第3个item只是一个例子,每一个app,它的那个app library path的最后一个部分都会是特定于那个app的。至于说,构造BaseDexClassLoader时的那个libraryPath 到底是怎么来的,那可能就会牵扯到android本身更复杂的一些过程了,在此不再做更详细的说明。

    Native 层load library的过程

    然后来看一下native层,把so文件load起的过程,先来一下nativeLoad()这个函数的实现(在JellyBean/dalvik/vm/native/java_lang_Runtime.cpp这个文件中):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    /*
     * static String nativeLoad(String filename, ClassLoader loader)
     *
     * Load the specified full path as a dynamic library filled with
     * JNI-compatible methods. Returns null on success, or a failure
     * message on failure.
     */
    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);
    }

    可以看到,nativeLoad()实际上只是完成了两件事情,第一,是调用dvmCreateCstrFromString()将Java 的library path String 转换到native的String,然后将这个path传给dvmLoadNativeCode()做load,dvmLoadNativeCode()这个函数的实现在dalvik/vm/Native.cpp中,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    /*
     * Load native code from the specified absolute pathname.  Per the spec,
     * if we've already loaded a library with the specified pathname, we
     * return without doing anything.
     *
     * TODO? for better results we should absolutify the pathname.  For fully
     * correct results we should stat to get the inode and compare that.  The
     * existing implementation is fine so long as everybody is using
     * System.loadLibrary.
     *
     * The library will be associated with the specified class loader.  The JNI
     * spec says we can't load the same library into more than one class loader.
     *
     * Returns "true" on success. On failure, sets *detail to a
     * human-readable description of the error or NULL if no detail is
     * available; ownership of the string is transferred to the caller.
     */
    bool dvmLoadNativeCode(const char* pathName, Object* classLoader,
            char** detail)
    {
        SharedLib* pEntry;
        void* handle;
        bool verbose;
     
        /* reduce noise by not chattering about system libraries */
        verbose = !!strncmp(pathName, "/system", sizeof("/system")-1);
        verbose = verbose && !!strncmp(pathName, "/vendor", sizeof("/vendor")-1);
     
        if (verbose)
            ALOGD("Trying to load lib %s %p", pathName, classLoader);
     
        *detail = NULL;
     
        /*
         * See if we've already loaded it.  If we have, and the class loader
         * matches, return successfully without doing anything.
         */
        pEntry = findSharedLibEntry(pathName);
        if (pEntry != NULL) {
            if (pEntry->classLoader != classLoader) {
                ALOGW("Shared lib '%s' already opened by CL %p; can't open in %p",
                    pathName, pEntry->classLoader, classLoader);
                return false;
            }
            if (verbose) {
                ALOGD("Shared lib '%s' already loaded in same CL %p",
                    pathName, classLoader);
            }
            if (!checkOnLoadResult(pEntry))
                return false;
            return true;
        }
     
        /*
         * Open the shared library.  Because we're using a full path, the system
         * doesn't have to search through LD_LIBRARY_PATH.  (It may do so to
         * resolve this library's dependencies though.)
         *
         * Failures here are expected when java.library.path has several entries
         * and we have to hunt for the lib.
         *
         * The current version of the dynamic linker prints detailed information
         * about dlopen() failures.  Some things to check if the message is
         * cryptic:
         *   - make sure the library exists on the device
         *   - verify that the right path is being opened (the debug log message
         *     above can help with that)
         *   - check to see if the library is valid (e.g. not zero bytes long)
         *   - check config/prelink-linux-arm.map to ensure that the library
         *     is listed and is not being overrun by the previous entry (if
         *     loading suddenly stops working on a prelinked library, this is
         *     a good one to check)
         *   - write a trivial app that calls sleep() then dlopen(), attach
         *     to it with "strace -p <pid>" while it sleeps, and watch for
         *     attempts to open nonexistent dependent shared libs
         *
         * This can execute slowly for a large library on a busy system, so we
         * want to switch from RUNNING to VMWAIT while it executes.  This allows
         * the GC to ignore us.
         */
        Thread* self = dvmThreadSelf();
        ThreadStatus oldStatus = dvmChangeStatus(self, THREAD_VMWAIT);
        handle = dlopen(pathName, RTLD_LAZY);
        dvmChangeStatus(self, oldStatus);
     
        if (handle == NULL) {
            *detail = strdup(dlerror());
            ALOGE("dlopen("%s") failed: %s", pathName, *detail);
            return false;
        }
     
        /* create a new entry */
        SharedLib* pNewEntry;
        pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib));
        pNewEntry->pathName = strdup(pathName);
        pNewEntry->handle = handle;
        pNewEntry->classLoader = classLoader;
        dvmInitMutex(&pNewEntry->onLoadLock);
        pthread_cond_init(&pNewEntry->onLoadCond, NULL);
        pNewEntry->onLoadThreadId = self->threadId;
     
        /* try to add it to the list */
        SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);
     
        if (pNewEntry != pActualEntry) {
            ALOGI("WOW: we lost a race to add a shared lib (%s CL=%p)",
                pathName, classLoader);
            freeSharedLibEntry(pNewEntry);
            return checkOnLoadResult(pActualEntry);
        } else {
            if (verbose)
                ALOGD("Added shared lib %s %p", pathName, classLoader);
     
            bool result = true;
            void* vonLoad;
            int version;
     
            vonLoad = dlsym(handle, "JNI_OnLoad");
            if (vonLoad == NULL) {
                ALOGD("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) {
                    ALOGI("[Calling JNI_OnLoad for "%s"]", pathName);
                }
                version = (*func)(gDvmJni.jniVm, NULL);
                dvmChangeStatus(self, oldStatus);
                self->classLoaderOverride = prevOverride;
     
                if (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 &&
                    version != JNI_VERSION_1_6)
                {
                    ALOGW("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) {
                        ALOGI("[Returned from JNI_OnLoad for "%s"]", pathName);
                    }
                }
            }
     
            if (result)
                pNewEntry->onLoadResult = kOnLoadOkay;
            else
                pNewEntry->onLoadResult = kOnLoadFailed;
     
            pNewEntry->onLoadThreadId = 0;
     
            /*
             * Broadcast a wakeup to anybody sleeping on the condition variable.
             */
            dvmLockMutex(&pNewEntry->onLoadLock);
            pthread_cond_broadcast(&pNewEntry->onLoadCond);
            dvmUnlockMutex(&pNewEntry->onLoadLock);
            return result;
        }
    }

    哇塞,dvmLoadNativeCode()这个函数还真的是有点复杂,那就挑那些跟我们的JNI比较紧密相关的逻辑来看吧。可以认为这个函数做了下面的这样一些事情:

    1. 调用dlopen() 打开一个so文件,创建一个handle。
    2. 调用dlsym()函数,查找到so文件中的JNI_OnLoad()这个函数的函数指针。
    3. 执行上一步找到的那个JNI_OnLoad()函数。

    至此,大体可以结束System.loadLibrary()的执行过程的分析。

  • 相关阅读:
    Mysql 创建联合主键
    Shell中的while循环
    shell 日期加减运算
    PHP日期格式转时间戳
    Uber 叫车时,弹出以下代码导致无法打车(An email confirmation has been sent to...),解决办法
    如何让Table显示滚动条
    mySQL中replace的用法
    打豪车应用:uber详细攻略(附100元优惠码)
    svn 命令行创建和删除 分支和tags
    php ob_start()、ob_end_flush和ob_end_clean()多级缓冲
  • 原文地址:https://www.cnblogs.com/misybing/p/4841010.html
Copyright © 2011-2022 走看看