Dalvik虚拟机JNI方法的注册过程分析

简介:

 在前面一文中,我们分析了Dalvik虚拟机的运行过程。从中可以知道,Dalvik虚拟机在调用一个成员函数的时候,如果发现该成员函数是一个JNI方法,那么就会直接跳到它的地址去执行。也就是说,JNI方法是直接在本地操作系统上执行的,而不是由Dalvik虚拟机解释器执行。由此也可看出,JNI方法是Android应用程序与本地操作系统直接进行通信的一个手段。在本文中,我们就详细分析JNI方法的注册过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

       在Android系统中,JNI方法是以C/C++语言来实现的,然后编译在一个SO文件里面。这个JNI方法在能够被调用之前,首先要加载到当前应用程序进程的地址空间来,如下所示:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
package  shy.luo.jni;
public  class  ClassWithJni {
     ......
                                                                         
     static  {
         System.loadLibrary( "nanosleep" );
     }
                                                                         
     ......
                                                                         
     private  native  int  nanosleep( long  seconds,  long  nanoseconds);
                                                                         
     ......
}


       上述代码假设ClassWithJni类有一个JNI方法nanosleep,它实现在一个名称为libnanosleep.so的文件中,因此,在该JNI方法能够被调用之前,我们首先要将它加载到当前应用程序进程来,这是通过调用System类的静态成员函数loadLibrary来实现的。

       JNI方法nanosleep的实现如下所示:


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
#include  "jni.h"
#include  "JNIHelp.h"
#include <time.h>
static  jint shy_luo_jni_ClassWithJni_nanosleep(JNIEnv* env, jobject clazz, jlong seconds, jlong nanoseconds)
{
     struct timespec req;
     req.tv_sec  = seconds;
     req.tv_nsec = nanoseconds;
                                                                     
     return  nanosleep(&req, NULL);
}
static  const  JNINativeMethod method_table[] = {
     { "nanosleep" "(JJ)I" , ( void *)shy_luo_jni_ClassWithJni_nanosleep},
};
extern  "C"  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  result;
     }
     jniRegisterNativeMethods(env,  "shy/luo/jni/ClassWithJni" , method_table, NELEM(method_table));
                                                                     
     return  JNI_VERSION_1_4;
}


       假设上述函数经过编译之后,就位于一个名称为libnanosleep.so的文件。

       当libnanosleep.so文件被加载的时候,函数JNI_OnLoad就会被调用。在函数JNI_OnLoad中,参数vm描述的是当前进程中的Dalvik虚拟机,通过调用它的成员函数GetEnv就可以获得一个JNIEnv对象。有了这个JNIEnv对象之后,我们就可以调用另外一个函数jniRegisterNativeMethods来向当前进程中的Dalvik虚拟机注册一个JNI方法shy_luo_jni_ClassWithJni_nanosleep。这个JNI方法即为shy.luo.jni.ClassWithJni类的成员函数nanasleep的实现。

       JNI方法shy_luo_jni_ClassWithJni_nanosleep要做的事情实际上就是通过系统调用nanosleep来使得当前进程进入睡眠状态,直至seconds秒nanoseconds纳秒之后再唤醒。使用系统调用nanosleep来使得当前进程进入睡眠状态的好处它的时间精度可以达到纳秒级,但是这个系统调用有两个地方是需要注意的:

       1. 如果进程在睡眠的过程中接收到信号,那么它就会提前被唤醒,这时候系统调用nanosleep的返回值为-1,并且错误代码errno被设置为EINTR。

       2. 如果CPU的时钟中断精度达不到纳秒级别,那么nanosleep的睡眠精度也达不到纳秒级,也就是说,当前进程不一定能在指定的纳秒之后被唤醒,会有一定的延时。

       不过,JNI方法shy_luo_jni_ClassWithJni_nanosleep的实现不是我们的重点,我们的重点是分析它注册到Dalvik虚拟机的过程。

       前面提到,JNI方法shy_luo_jni_ClassWithJni_nanosleep是libnanosleep.so文件加载的时候被注册到Dalvik虚拟机的,因此,我们就从libnanosleep.so文件的加载开始,分析JNI方法shy_luo_jni_ClassWithJni_nanosleep注册到Dalvik虚拟机的过程,也就是从System类的静态成员函数loadLibrary开始分析一个JNI方法注册到Dalvik虚拟机的过程,如图1所示:


图1 JNI方法注册到Dalvik虚拟机的过程

       这个过程可以分为12个步骤,接下来我们就详细分析每一个步骤。

       Step 1. System.loadLibrary


1
2
3
4
5
6
7
8
9
10
11
12
public  final  class  System {
     ......
                                                                 
     public  static  void  loadLibrary(String libName) {
         SecurityManager smngr = System.getSecurityManager();
         if  (smngr !=  null ) {
             smngr.checkLink(libName);
         }
         Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
     }
     ......
}

       这个函数定义在文件libcore/luni/src/main/java/java/lang/System.java中。

       System类的成员函数loadLibrary首先调用SecurityManager类的成员函数checkLink来进行安全检查,即检查名称为libName的so文件是否允许加载。注意,这是Java的安全代码检查机制,而不是Android系统的安全检查机制,而且Android系统没有使用它来进行安全检查。因此,这个检查总是能通过的。

       System类的成员函数loadLibrary接下来就再通过运行时类Runtime的成员函数loadLibrary来加载名称为libName的so文件,接下来我们就继续分析它的实现。

       Step 2. Runtime.loadLibrary


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
public  class  Runtime {
     ......
     void  loadLibrary(String libraryName, ClassLoader loader) {
         if  (loader !=  null ) {
             String filename = loader.findLibrary(libraryName);
             if  (filename ==  null ) {
                 throw  new  UnsatisfiedLinkError( "Couldn't load "  + libraryName +  ": "  +
                         "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);
     }
     ......
}

       这个函数定义在文件libcore/luni/src/main/java/java/lang/Runtime.java中。


       在Runtime类的成员函数loadLibrary中,参数libraryName表示要加载的so文件,而参数loader表示与要加载的so文件所关联的类的一个类加载器。例如,在我们这个情景中,libraryName等于“nanosleep”,与它所关联的类为shy.luo.jni.ClassWithJni。每一类有一个关联的类加载器,用来负责加载该类。在Dalvik虚拟机中,类加载器除了知道它要加载的类所在的文件路径之外,还知道该类所属的APK用来保存so文件的路径。因此,给定一个so文件名称,一个类加载器可以判断它是否存在自己的so文件目录中。

       参数libraryName只是描述要加载的so文件的部分名称,它的完整名称需要根据本地操作系统的特证来确定。由于目前Android系统都是属于Linux系统,而在Linux系统中,so文件的命名规范通常就是lib<name>.so的形式,因此,在我们这个情景中,名称为“nanosleep”的so文件的完整名称就为“libnanosleep.so”,这是通过调用System类的静态成员函数mapLibraryName来获得的。

       上面所获得的libnanosleep.so文件的名称仍然还不够完整,因为它没有包含绝对路径。在这种情况下,我们是无法将它加载到Dalvik虚拟机中去的。当参数loader的值不等于null的时候,Runtime类的成员函数loadLibrary就会调用它的成员函数findLibrary来它的so文件目录中寻找是否有一外名称为“libnanosleep.so”。如果存在的话,那么就会返回该libnanosleep.so文件的绝对路径。有了libnanosleep.so文件的绝对路径之后,就可以调用Runtime类的另外一个成员函数nativeLoad来将它加载到当前进程的Dalvik虚拟机中。注意,将参数libraryName转换为lib<name>.so的完整形式,以及获得该so文件的绝对路径,都是由参数loader所描述的一个类加载器的成员函数findLibrary来完成的。

       另一方面,如果参数loader的值等于null,那么就表示当前要加载的so文件要在系统范围的so文件目录查找。这些系统范围的so文件目录保存在Runtime类的成员变量mLibPaths所描述的一个String数组中。通过依次检查这些目录是否存在与参数libraryName对应的so文件,就可以确定参数libraryName所指定加载的so文件是否是一个合法的so文件。如果合法的话,那么同样会调用Runtime类的另外一个成员函数nativeLoad来将它加载到当前进程的Dalvik虚拟机中。注意,这里在检查参数libraryName所表示的so文件是否存在于系统范围的so文件目录之前,同样要将它转换为lib<name>.so的形式,这同样也是通过调用System类的静态成员函数mapLibraryName来完成的。

       如果最后无法在指定的APK或者系统范围的so文件目录中找到由参数libraryName所描述的so文件,或者找到了该so文件,但是在加载该so文件的过程中出现错误,那么Runtime类的成员函数loadLibrary都会抛出一个类型为UnsatisfiedLinkError的异常。

       由于加载参数libraryName所描述的so文件是由Runtime类的成员函数nativeLoad来实现的,因此,接下来我们继续分析它的实现。

       Step 3. Runtime.nativeLoad


1
2
3
4
5
public  class  Runtime {
     ......
     private  static  native  String nativeLoad(String filename, ClassLoader loader);
     ......
}

       这个函数定义在文件libcore/luni/src/main/java/java/lang/Runtime.java中。


       Runtime类的成员函数nativeLoad是一个JNI方法。由于该JNI方法是属于Java核心类Runtime的,也就是说,它在Dalvik虚拟机启动的时候就已经在内部注册过了,因此,这时候我们可以直接调用它注册其它的JNI方法,也就是so文件filename里面所指定的JNI方法。Dalvik虚拟机在启动过程中注册Java核心类的操作,具体可以参考前面Dalvik虚拟机的启动过程分析一文。

       Runtime类的成员函数nativeLoad在C++层对应的函数为Dalvik_java_lang_Runtime_nativeLoad,如下所示:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
}

       这个函数定义在文件dalvik/vm/native/java_lang_Runtime.c中。


       参数args[0]保存的是一个Java层的String对象,这个String对象描述的就是要加载的so文件,函数Dalvik_java_lang_Runtime_nativeLoad首先是调有函数dvmCreateCstrFromString来将它转换成一个C++层的字符串fileName,然后再调用函数dvmLoadNativeCode来执行加载so文件的操作。

       接下来,我们就继续分函数dvmLoadNativeCode的实现,以便可以了解一个so文件的加载过程。

       Step 4. dvmLoadNativeCode


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
bool dvmLoadNativeCode( const  char * pathName, Object* classLoader,
         char ** detail)
{
     SharedLib* pEntry;
     void * handle;
     ......
     pEntry = findSharedLibEntry(pathName);
     if  (pEntry != NULL) {
         if  (pEntry->classLoader != classLoader) {
             ......
             return  false ;
         }
         ......
         if  (!checkOnLoadResult(pEntry))
             return  false ;
         return  true ;
     }
     ......
     handle = dlopen(pathName, RTLD_LAZY);
     ......
     /* create a new entry */
     SharedLib* pNewEntry;
     pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib));
     pNewEntry->pathName = strdup(pathName);
     pNewEntry->handle = handle;
     pNewEntry->classLoader = classLoader;
     ......
     /* try to add it to the list */
     SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);
                                            
     if  (pNewEntry != pActualEntry) {
         ......
         freeSharedLibEntry(pNewEntry);
         return  checkOnLoadResult(pActualEntry);
     else  {
         ......
         bool result =  true ;
         void * vonLoad;
         int  version;
         vonLoad = dlsym(handle,  "JNI_OnLoad" );
         if  (vonLoad == NULL) {
             LOGD( "No JNI_OnLoad found in %s %p, skipping init\n" ,
                 pathName, classLoader);
         else  {
             ......
             OnLoadFunc func = vonLoad;
             ......
             version = (*func)(gDvm.vmList, NULL);
             ......
             if  (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 &&
                 version != JNI_VERSION_1_6)
             {
                 .......
                 result =  false ;
             else  {
                 LOGV( "+++ finished JNI_OnLoad %s\n" , pathName);
             }
         }
         ......
         if  (result)
             pNewEntry->onLoadResult = kOnLoadOkay;
         else
             pNewEntry->onLoadResult = kOnLoadFailed;
         ......
         return  result;
     }
}

       这个函数定义在文件dalvik/vm/Native.c中。


       函数dvmLoadNativeCode首先是检查参数pathName所指定的so文件是否已经加载过了,这是通过调用函数findSharedLibEntry来实现的。如果已经加载过,那么就可以获得一个SharedLib对象pEntry。这个SharedLib对象pEntry描述了有关参数pathName所指定的so文件的加载信息,例如,上次用来加载它的类加载器和上次的加载结果。如果上次用来加载它的类加载器不等于当前所使用的类加载器,或者上次没有加载成功,那么函数dvmLoadNativeCode就回直接返回false给调用者,表示不能在当前进程中加载参数pathName所描述的so文件。

       我们假设参数pathName所指定的so文件还没有被加载过,这时候函数dvmLoadNativeCode就会先调用dlopen来在当前进程中加载它,并且将获得的句柄保存在变量handle中,接着再创建一个SharedLib对象pNewEntry来描述它的加载信息。这个SharedLib对象pNewEntry还会通过函数addSharedLibEntry被缓存起来,以便可以知道当前进程都加载了哪些so文件。

        注意,在调用函数addSharedLibEntry来缓存新创建的SharedLib对象pNewEntry的时候,如果得到的返回值pActualEntry指向的不是SharedLib对象pNewEntry,那么就表示另外一个线程也正在加载参数pathName所指定的so文件,并且比当前线程提前加载完成。在这种情况下,函数addSharedLibEntry就什么也不用做而直接返回了。否则的话,函数addSharedLibEntry就要继续负责调用前面所加载的so文件中的一个指定的函数来注册它里面的JNI方法。

       这个指定的函数的名称为“JNI_OnLoad”,也就是说,每一个用来实现JNI方法的so文件都应该定义有一个名称为“JNI_OnLoad”的函数,并且这个函数的原型为:


1
jint JNI_OnLoad(JavaVM* vm,  void * reserved);

       函数dvmLoadNativeCode通过调用函数dlsym就可以获得在前面加载的so中名称为“JNI_OnLoad”的函数的地址,最终保存在函数指针func中。有了这个函数指针之后,我们就可以直接调用它来执行注册JNI方法的操作了。注意,在调用该JNI_OnLoad函数时,第一个要传递进行的参数是一个JavaVM对象,这个JavaVM对象描述的是在当前进程中运行的Dalvik虚拟机,第二个要传递的参数可以设置为NULL,这是保留给以后使用的。


       从前面Dalvik虚拟机的启动过程分析一文可以知道,在当前进程所运行的Dalvik虚拟机实例是通过全局变量gDvm所描述的一个DvmGlobals结构体的成员变量vmList来描述的,因此,我们就可以将它传递在前面加载的so中名称中定义的JNI_OnLoad函数。注意,定义在该so文件中的JNI_OnLoad函数一旦执行成功,它的返回值就必须等于JNI_VERSION_1_2、JNI_VERSION_1_4或者JNI_VERSION_1_6,用来表示所注册的JNI方法的版本。

       最后, 函数dvmLoadNativeCode根据上述的JNI_OnLoad函数的执行成功与否,将前面所创建的一个SharedLib对象pNewEntry的成员变量onLoadResult设置为kOnLoadOkay或者kOnLoadFailed,这样就可以记录参数pathName所指定的so文件是否是加载成功的,也就是它是否成功地注册了其内部的JNI方法。

       在我们这个情景中,参数pathName所指定的so文件为libnanosleep.so,接下来我们就继续分析它的函数JNI_OnLoad的实现,以便可以发解定义在它里面的JNI方法的注册过程。

       Step 5. JNI_OnLoad

       定义在libnanosleep.so文件中的函数JNI_OnLoad的实现可以参考文章开始的部分。从它的实现可以知道,它所注册的JNI方法shy_luo_jni_ClassWithJni_nanosleep是与shy.luo.jni.ClassWithJni类的成员函数nanosleep对应的,并且是通过调用函数jniRegisterNativeMethods来实现的。因此,接下来我们就继续分析函数jniRegisterNativeMethods的实现。

       Step 6. jniRegisterNativeMethods


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int  jniRegisterNativeMethods(JNIEnv* env,  const  char * className,
     const  JNINativeMethod* gMethods,  int  numMethods)
{
     jclass clazz;
     LOGV( "Registering %s natives\n" , className);
     clazz = (*env)->FindClass(env, className);
     if  (clazz == NULL) {
         LOGE( "Native registration unable to find class '%s'\n" , className);
         return  - 1 ;
     }
     int  result =  0 ;
     if  ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) <  0 ) {
         LOGE( "RegisterNatives failed for '%s'\n" , className);
         result = - 1 ;
     }
     (*env)->DeleteLocalRef(env, clazz);
     return  result;
}

       这个函数定义在文件dalvik/libnativehelper/JNIHelp.c中。


       参数env所指向的一个JNIEnv结构体,通过调用这个JNIEnv结构体可以获得参数className所描述的一个类。这个类就是要注册JNI的类,而它所要注册的JNI就是由参数gMethods来描述的。

       注册参数gMethods所描述的JNI方法是通过调用env所指向的一个JNIEnv结构体的成员函数RegisterNatives来实现的,因此,接下来我们就继续分析它的实现。

       Step 7. JNIEnv.RegisterNatives


1
2
3
4
5
6
7
8
9
10
11
12
typedef _JNIEnv JNIEnv;
......
struct _JNIEnv {
     /* do not rename this; it does not seem to be entirely opaque */
     const  struct JNINativeInterface* functions;
     ......
     jint RegisterNatives(jclass clazz,  const  JNINativeMethod* methods,
         jint nMethods)
     return  functions->RegisterNatives( this , clazz, methods, nMethods); }
                              
     ......
}

       这个函数定义在文件dalvik/libnativehelper/include/nativehelper/jni.h中。


       从前面Dalvik虚拟机的运行过程分析一文可以知道,结构体JNIEnv的成员变量functions指向的是一个函数表,这个函数表又包含了一系列的函数指针,指向了在当前进程中运行的Dalvik虚拟机中定义的函数。对于结构体JNIEnv的成员函数RegisterNatives来说,它就是通过调用这个函数表中名称为RegisterNatives的函数指针来注册参数gMethods所描述的JNI方法的。

       从前面Dalvik虚拟机的启动过程分析一文可以知道,上述函数表中名称为RegisterNatives的函数指针指向的是在Dalvik虚拟机内部定义的函数RegisterNatives,因此,接下来我们就继续分析它的实现。

       Step 8. RegisterNatives


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static  jint RegisterNatives(JNIEnv* env, jclass jclazz,
     const  JNINativeMethod* methods, jint nMethods)
{
     JNI_ENTER();
     ClassObject* clazz = (ClassObject*) dvmDecodeIndirectRef(env, jclazz);
     jint retval = JNI_OK;
     int  i;
     ......
     for  (i =  0 ; i < nMethods; i++) {
         if  (!dvmRegisterJNIMethod(clazz, methods[i].name,