`
ydbc
  • 浏览: 718538 次
  • 性别: Icon_minigender_1
  • 来自: 大连
文章分类
社区版块
存档分类
最新评论

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

 
阅读更多

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

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

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

  1. packageshy.luo.jni;
  2. publicclassClassWithJni{
  3. ......
  4. static{
  5. System.loadLibrary("nanosleep");
  6. }
  7. ......
  8. privatenativeintnanosleep(longseconds,longnanoseconds);
  9. ......
  10. }

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

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

  1. #include"jni.h"
  2. #include"JNIHelp.h"
  3. #include<time.h>
  4. staticjintshy_luo_jni_ClassWithJni_nanosleep(JNIEnv*env,jobjectclazz,jlongseconds,jlongnanoseconds)
  5. {
  6. structtimespecreq;
  7. req.tv_sec=seconds;
  8. req.tv_nsec=nanoseconds;
  9. returnnanosleep(&req,NULL);
  10. }
  11. staticconstJNINativeMethodmethod_table[]={
  12. {"nanosleep","(JJ)I",(void*)shy_luo_jni_ClassWithJni_nanosleep},
  13. };
  14. extern"C"jintJNI_OnLoad(JavaVM*vm,void*reserved)
  15. {
  16. JNIEnv*env=NULL;
  17. jintresult=-1;
  18. if(vm->GetEnv((void**)&env,JNI_VERSION_1_4)!=JNI_OK){
  19. returnresult;
  20. }
  21. jniRegisterNativeMethods(env,"shy/luo/jni/ClassWithJni",method_table,NELEM(method_table));
  22. returnJNI_VERSION_1_4;
  23. }

假设上述函数经过编译之后,就位于一个名称为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. publicfinalclassSystem{
  2. ......
  3. publicstaticvoidloadLibrary(StringlibName){
  4. SecurityManagersmngr=System.getSecurityManager();
  5. if(smngr!=null){
  6. smngr.checkLink(libName);
  7. }
  8. Runtime.getRuntime().loadLibrary(libName,VMStack.getCallingClassLoader());
  9. }
  10. ......
  11. }

这个函数定义在文件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. publicclassRuntime{
  2. ......
  3. voidloadLibrary(StringlibraryName,ClassLoaderloader){
  4. if(loader!=null){
  5. Stringfilename=loader.findLibrary(libraryName);
  6. if(filename==null){
  7. thrownewUnsatisfiedLinkError("Couldn'tload"+libraryName+":"+
  8. "findLibraryreturnednull");
  9. }
  10. Stringerror=nativeLoad(filename,loader);
  11. if(error!=null){
  12. thrownewUnsatisfiedLinkError(error);
  13. }
  14. return;
  15. }
  16. Stringfilename=System.mapLibraryName(libraryName);
  17. List<String>candidates=newArrayList<String>();
  18. StringlastError=null;
  19. for(Stringdirectory:mLibPaths){
  20. Stringcandidate=directory+filename;
  21. candidates.add(candidate);
  22. if(newFile(candidate).exists()){
  23. Stringerror=nativeLoad(candidate,loader);
  24. if(error==null){
  25. return;//Wesuccessfullyloadedthelibrary.Jobdone.
  26. }
  27. lastError=error;
  28. }
  29. }
  30. if(lastError!=null){
  31. thrownewUnsatisfiedLinkError(lastError);
  32. }
  33. thrownewUnsatisfiedLinkError("Library"+libraryName+"notfound;tried"+candidates);
  34. }
  35. ......
  36. }
这个函数定义在文件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. publicclassRuntime{
  2. ......
  3. privatestaticnativeStringnativeLoad(Stringfilename,ClassLoaderloader);
  4. ......
  5. }
这个函数定义在文件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. staticvoidDalvik_java_lang_Runtime_nativeLoad(constu4*args,
  2. JValue*pResult)
  3. {
  4. StringObject*fileNameObj=(StringObject*)args[0];
  5. Object*classLoader=(Object*)args[1];
  6. char*fileName=NULL;
  7. StringObject*result=NULL;
  8. char*reason=NULL;
  9. boolsuccess;
  10. assert(fileNameObj!=NULL);
  11. fileName=dvmCreateCstrFromString(fileNameObj);
  12. success=dvmLoadNativeCode(fileName,classLoader,&reason);
  13. if(!success){
  14. constchar*msg=(reason!=NULL)?reason:"unknownfailure";
  15. result=dvmCreateStringFromCstr(msg);
  16. dvmReleaseTrackedAlloc((Object*)result,NULL);
  17. }
  18. free(reason);
  19. free(fileName);
  20. RETURN_PTR(result);
  21. }
这个函数定义在文件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. booldvmLoadNativeCode(constchar*pathName,Object*classLoader,
  2. char**detail)
  3. {
  4. SharedLib*pEntry;
  5. void*handle;
  6. ......
  7. pEntry=findSharedLibEntry(pathName);
  8. if(pEntry!=NULL){
  9. if(pEntry->classLoader!=classLoader){
  10. ......
  11. returnfalse;
  12. }
  13. ......
  14. if(!checkOnLoadResult(pEntry))
  15. returnfalse;
  16. returntrue;
  17. }
  18. ......
  19. handle=dlopen(pathName,RTLD_LAZY);
  20. ......
  21. /*createanewentry*/
  22. SharedLib*pNewEntry;
  23. pNewEntry=(SharedLib*)calloc(1,sizeof(SharedLib));
  24. pNewEntry->pathName=strdup(pathName);
  25. pNewEntry->handle=handle;
  26. pNewEntry->classLoader=classLoader;
  27. ......
  28. /*trytoaddittothelist*/
  29. SharedLib*pActualEntry=addSharedLibEntry(pNewEntry);
  30. if(pNewEntry!=pActualEntry){
  31. ......
  32. freeSharedLibEntry(pNewEntry);
  33. returncheckOnLoadResult(pActualEntry);
  34. }else{
  35. ......
  36. boolresult=true;
  37. void*vonLoad;
  38. intversion;
  39. vonLoad=dlsym(handle,"JNI_OnLoad");
  40. if(vonLoad==NULL){
  41. LOGD("NoJNI_OnLoadfoundin%s%p,skippinginit\n",
  42. pathName,classLoader);
  43. }else{
  44. ......
  45. OnLoadFuncfunc=vonLoad;
  46. ......
  47. version=(*func)(gDvm.vmList,NULL);
  48. ......
  49. if(version!=JNI_VERSION_1_2&&version!=JNI_VERSION_1_4&&
  50. version!=JNI_VERSION_1_6)
  51. {
  52. .......
  53. result=false;
  54. }else{
  55. LOGV("+++finishedJNI_OnLoad%s\n",pathName);
  56. }
  57. }
  58. ......
  59. if(result)
  60. pNewEntry->onLoadResult=kOnLoadOkay;
  61. else
  62. pNewEntry->onLoadResult=kOnLoadFailed;
  63. ......
  64. returnresult;
  65. }
  66. }
这个函数定义在文件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. jintJNI_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. intjniRegisterNativeMethods(JNIEnv*env,constchar*className,
  2. constJNINativeMethod*gMethods,intnumMethods)
  3. {
  4. jclassclazz;
  5. LOGV("Registering%snatives\n",className);
  6. clazz=(*env)->FindClass(env,className);
  7. if(clazz==NULL){
  8. LOGE("Nativeregistrationunabletofindclass'%s'\n",className);
  9. return-1;
  10. }
  11. intresult=0;
  12. if((*env)->RegisterNatives(env,clazz,gMethods,numMethods)<0){
  13. LOGE("RegisterNativesfailedfor'%s'\n",className);
  14. result=-1;
  15. }
  16. (*env)->DeleteLocalRef(env,clazz);
  17. returnresult;
  18. }
这个函数定义在文件dalvik/libnativehelper/JNIHelp.c中。

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

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

Step 7.JNIEnv.RegisterNatives

  1. typedef_JNIEnvJNIEnv;
  2. ......
  3. struct_JNIEnv{
  4. /*donotrenamethis;itdoesnotseemtobeentirelyopaque*/
  5. conststructJNINativeInterface*functions;
  6. ......
  7. jintRegisterNatives(jclassclazz,constJNINativeMethod*methods,
  8. jintnMethods)
  9. {returnfunctions->RegisterNatives(this,clazz,methods,nMethods);}
  10. ......
  11. }
这个函数定义在文件dalvik/libnativehelper/include/nativehelper/jni.h中。

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

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

Step 8.RegisterNatives

  1. staticjintRegisterNatives(JNIEnv*env,jclassjclazz,
  2. constJNINativeMethod*methods,jintnMethods)
  3. {
  4. JNI_ENTER();
  5. ClassObject*clazz=(ClassObject*)dvmDecodeIndirectRef(env,jclazz);
  6. jintretval=JNI_OK;
  7. inti;
  8. ......
  9. for(i=0;i<nMethods;i++){
  10. if(!dvmRegisterJNIMethod(clazz,methods[i].name,
  11. methods[i].signature,methods[i].fnPtr))
  12. {
  13. retval=JNI_ERR;
  14. }
  15. }
  16. JNI_EXIT();
  17. returnretval;
  18. }

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

参数jclazz描述的是要注册JNI方法的类,而参数methods描述的是要注册的一组JNI方法,这个组JNI方法的个数由参数nMethods来描述。

函数RegisterNatives首先是调用函数dvmDecodeIndirectRef来获得要注册JNI方法的类对象,接着再通过一个for循环来依次调用函数dvmRegisterJNIMethod注册参数methods描述所描述的每一个JNI方法。注意,每一个JNI方法都由名称、签名和地址来描述。

接下来,我们就继续分析函数dvmRegisterJNIMethod的实现。

Step 9.dvmRegisterJNIMethod

  1. staticbooldvmRegisterJNIMethod(ClassObject*clazz,constchar*methodName,
  2. constchar*signature,void*fnPtr)
  3. {
  4. Method*method;
  5. boolresult=false;
  6. if(fnPtr==NULL)
  7. gotobail;
  8. method=dvmFindDirectMethodByDescriptor(clazz,methodName,signature);
  9. if(method==NULL)
  10. method=dvmFindVirtualMethodByDescriptor(clazz,methodName,signature);
  11. if(method==NULL){
  12. LOGW("ERROR:Unabletofinddeclfornative%s.%s:%s\n",
  13. clazz->descriptor,methodName,signature);
  14. gotobail;
  15. }
  16. if(!dvmIsNativeMethod(method)){
  17. LOGW("Unabletoregister:notnative:%s.%s:%s\n",
  18. clazz->descriptor,methodName,signature);
  19. gotobail;
  20. }
  21. if(method->nativeFunc!=dvmResolveNativeMethod){
  22. /*thisisallowed,butunusual*/
  23. LOGV("Note:%s.%s:%swasalreadyregistered\n",
  24. clazz->descriptor,methodName,signature);
  25. }
  26. dvmUseJNIBridge(method,fnPtr);
  27. LOGV("JNI-registered%s.%s:%s\n",clazz->descriptor,methodName,
  28. signature);
  29. result=true;
  30. bail:
  31. returnresult;
  32. }
这个函数定义在文件dalvik/vm/Jni.c中。

函数dvmRegisterJNIMethod在注册参数methodName所描述的JNI方法之前,首先会进行一系列的检查,包括:

1. 确保参数clazz所描述的类有一个名称为methodName的成员函数。首先是调用函数dvmFindDirectMethodByDescriptor来检查methodName是否是clazz的一个非虚成员函数,然后再调用函数dvmFindVirtualMethodByDescriptor来检查methodName是否是clazz的一个虚成员函数。

2. 确保类clazz的成员函数methodName确实是声明为JNI方法,即带有native修饰符,这是通过调用函数dvmIsNativeMethod来实现的。

通过了前面的第1个检查之后,就可以获得一个Method对象method,用来描述要注册的JNI方法所对应的Java类成员函数。当一个Method对象method描述的是一个JNI方法的时候,它的成员变量nativeFunc保存的就是该JNI方法的地址,但是在对应的JNI方法注册进来之前,该成员变量的值被统一设置为dvmResolveNativeMethod。因此,当我们调用了一个未注册的JNI方法时,实际上执行的是函数dvmResolveNativeMethod。函数dvmResolveNativeMethod此时会在Dalvik虚拟内部以及当前所有已经加载的共享库中检查是否存在对应的JNI方法。如果不存在,那么它就会抛出一个类型为java.lang.UnsatisfiedLinkError的异常。

注意,一个JNI方法是可以重复注册的,无论如何,函数dvmRegisterJNIMethod都是调用另外一个函数dvmUseJNIBridge来继续执行注册JNI的操作。

Step 10.dvmUseJNIBridge

  1. /**
  2. *Returnstrueifthe-Xjnitracesettingimpliesweshouldtrace'method'.
  3. */
  4. staticboolshouldTrace(Method*method)
  5. {
  6. returngDvm.jniTrace&&strstr(method->clazz->descriptor,gDvm.jniTrace);
  7. }
  8. /*
  9. *Point"method->nativeFunc"attheJNIbridge,andoverload"method->insns"
  10. *topointattheactualfunction.
  11. */
  12. voiddvmUseJNIBridge(Method*method,void*func)
  13. {
  14. DalvikBridgeFuncbridge=shouldTrace(method)
  15. ?dvmTraceCallJNIMethod
  16. :dvmSelectJNIBridge(method);
  17. dvmSetNativeFunc(method,bridge,func);
  18. }

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

一个JNI方法并不是直接被调用的,而是通过由Dalvik虚拟机间接地调用,这个用来间接调用JNI方法的函数就称为一个Bridge。这些Bridage函数在真正调用JNI方法之前,会执行一些通用的初始化工作。例如,会将当前线程的状态设置为NATIVE,因为它即将要执行一个Native函数。又如,会为即将要被调用的JNI方法准备好前面两个参数,第一个参数是一个JNIEnv对象,用来描述当前线程的Java环境,通过它可以访问反过来访问Java代码和Java对象,第二个参数是一个jobject对象,用来描述当前正在执行JNI方法的Java对象。

这些Bridage函数实际上仍然不是直接调用地调用JNI方法的,这是因为Dalvik虚拟机是可以运行在各种不同的平台之上,而每一种平台可能都定义有自己的一套函数调用规范,也就是所谓的ABI(Application Binary Interface),这是一个API(Application Programming Interface)不同的概念。ABI是在二进制级别上定义的一套函数调用规范,例如参数是通过寄存器来传递还是堆栈来传递,而API定义是一个应用程序编程接口规范。换句话说,API定义了源代码和库之间的接口,因此同样的代码可以在支持这个API的任何系统中编译 ,而ABI允许编译好的目标代码在使用兼容ABI的系统中无需改动就能运行。

为了使得运行在不同平台上的Dalvik虚拟机能够以统一的方法来调用JNI方法,这些Bridage函数使用了一个libffi库,它的源代码位于external/libffi目录中。Libffi是一个开源项目,用于高级语言之间的相互调用的处理,它的实现机制可以进一步参考http://www.sourceware.org/libffi/

回到函数dvmUseJNIBridge中,它主要就是根据Dalvik虚拟机的启动选项来为即将要注册的JNI选择一个合适的Bridge函数。如果我们在Dalvik虚拟机启动的时候,通过-Xjnitrace选项来指定了要跟踪参数method所描述的JNI方法,那么函数dvmUseJNIBridge为该JNI方法选择的Bridge函数就为dvmTraceCallJNIMethod,否则的话,就再通过另外一个函数dvmSelectJNIBridge来进一步选择一个合适的Bridge函数。选择好Bridge函数之后,函数dvmUseJNIBridge最终就调用函数dvmSetNativeFunc来执行真正的JNI方法注册操作。

我们假设参数method所描述的JNI方法没有设置为跟踪,因此,接下来,我们就首先分析函数dvmSelectJNIBridge的实现,接着再分析函数dvmSetNativeFunc的实现。

Step 11.dvmSelectJNIBridge

  1. /*
  2. *ReturnstheappropriateJNIbridgefor'method',alsotakingintoaccount
  3. *the-Xcheck:jnisetting.
  4. */
  5. staticDalvikBridgeFuncdvmSelectJNIBridge(constMethod*method)
  6. {
  7. enum{
  8. kJNIGeneral=0,
  9. kJNISync=1,
  10. kJNIVirtualNoRef=2,
  11. kJNIStaticNoRef=3,
  12. }kind;
  13. staticconstDalvikBridgeFuncstdFunc[]={
  14. dvmCallJNIMethod_general,
  15. dvmCallJNIMethod_synchronized,
  16. dvmCallJNIMethod_virtualNoRef,
  17. dvmCallJNIMethod_staticNoRef
  18. };
  19. staticconstDalvikBridgeFunccheckFunc[]={
  20. dvmCheckCallJNIMethod_general,
  21. dvmCheckCallJNIMethod_synchronized,
  22. dvmCheckCallJNIMethod_virtualNoRef,
  23. dvmCheckCallJNIMethod_staticNoRef
  24. };
  25. boolhasRefArg=false;
  26. if(dvmIsSynchronizedMethod(method)){
  27. /*useversionwithsynchronization;callsintogeneralhandler*/
  28. kind=kJNISync;
  29. }else{
  30. /*
  31. *Doaquickscanthroughthe"shorty"signaturetoseeifthemethod
  32. *takesanyreferencearguments.
  33. */
  34. constchar*cp=method->shorty;
  35. while(*++cp!='\0'){/*pre-incrtoskipreturntype*/
  36. if(*cp=='L'){
  37. /*'L'usedforbothobjectandarrayreferences*/
  38. hasRefArg=true;
  39. break;
  40. }
  41. }
  42. if(hasRefArg){
  43. /*usegeneralhandlertoslurpupreferenceargs*/
  44. kind=kJNIGeneral;
  45. }else{
  46. /*virtualmethodshavearefinargs[0](notinsignature)*/
  47. if(dvmIsStaticMethod(method))
  48. kind=kJNIStaticNoRef;
  49. else
  50. kind=kJNIVirtualNoRef;
  51. }
  52. }
  53. returndvmIsCheckJNIEnabled()?checkFunc[kind]:stdFunc[kind];
  54. }
这个函数定义在文件dalvik/vm/Jni.c中。

Dalvik虚拟机提供的Bridge函数主要是分为两类。第一类Bridge函数在调用完成JNI方法之后,会检查该JNI方法的返回结果是否与声明的一致,这是因为一个声明返回String的JNI方法在执行时返回的可能会是一个Byte Array。如果不一致,取决于Dalvik虚拟机的启动选项,它可能会停机。第二类Bridge函数不对JNI方法的返回结果进行上述检查。选择哪一类Bridge函数可以通过-Xcheck:jni选项来决定。不过由于检查一个JNI方法的返回结果是否与声明的一致是很耗时的,因此,我们一般都不会使用第一类Bridge函数。

此外,每一类Bridge函数又分为四个子类:Genernal、Sync、VirtualNoRef和StaticNoRef,它们的选择规则为:

1. 一个JNI方法的参数列表中如果包含有引用类型的参数,那么对应的Bridge函数就是Genernal类型的,即为dvmCallJNIMethod_general或者dvmCheckCallJNIMethod_general。

2. 一个JNI方法如果声明为同步方法,即带有synchronized修饰符,那么对应的Bridge函数就是Sync类型的,即为dvmCallJNIMethod_synchronized或者dvmCheckCallJNIMethod_synchronized。

3.一个JNI方法的参数列表中如果不包含有引用类型的参数,并且它是一个虚成员函数,那么对应的Bridge函数就是kJNIVirtualNoRef类型的,即为dvmCallJNIMethod_virtualNoRef或者dvmCheckCallJNIMethod_virtualNoRef。

4. 一个JNI方法的参数列表中如果不包含有引用类型的参数,并且它是一个静态成员函数,那么对应的Bridge函数就是StaticNoRef类型的,即为dvmCallJNIMethod_staticNoRef或者dvmCheckCallJNIMethod_staticNoRef。

每一类Bridge函数之所以要划分为上述四个子类,是因为每一个子类的Bridge函数在调用真正的JNI方法之前,所要进行的准备工作是不一样的。例如,Genernal类型的Bridge函数需要为引用类型的参数增加一个本地引用,避免它在JNI方法执行的过程中被回收。又如,Sync类型的Bridge函数在调用JNI方法之前,需要执行同步原始,以避免多线程访问的竞争问题。

这一步执行完成之后,返回到前面的Step 10中,即函数dvmUseJNIBridge中,这时候它就获得了一个Bridge函数,因此,接下来它就可以调用函数dvmSetNativeFunc来执行真正的JNI方法注册操作了。

Step 12.dvmSetNativeFunc

  1. voiddvmSetNativeFunc(Method*method,DalvikBridgeFuncfunc,
  2. constu2*insns)
  3. {
  4. ......
  5. if(insns!=NULL){
  6. /*updateboth,ensuringthat"insns"isobservedfirst*/
  7. method->insns=insns;
  8. android_atomic_release_store((int32_t)func,
  9. (void*)&method->nativeFunc);
  10. }else{
  11. /*onlyupdatenativeFunc*/
  12. method->nativeFunc=func;
  13. }
  14. ......
  15. }
这个函数定义在文件dalvik/vm/oo/Class.c中。

参数method表示要注册JNI方法的Java类成员函数,参数func表示JNI方法的Bridge函数,参数insns表示要注册的JNI方法的函数地址。

当参数insns的值不等于NULL的时候,函数dvmSetNativeFunc就分别将参数insns和func的值分别保存在参数method所指向的一个Method对象的成员变量insns和nativeFunc中,而当insns的值等于NULL的时候,函数dvmSetNativeFunc就只将参数func的值保存在参数method所指向的一个Method对象成员变量nativeFunc中。

假设在前面的Step 11中选择的Bridge函数为dvmCallJNIMethod_general,并且结合前面Dalvik虚拟机的运行过程分析一文,我们就可以得到Dalvik虚拟机在运行过程中调用JNI方法的过程:

1. 调用函数dvmCallJNIMethod_general,执行一些必要的准备工作;

2.函数dvmCallJNIMethod_general再调用函数dvmPlatformInvoke来以统一的方式来调用对应的JNI方法;

3.函数dvmPlatformInvoke通过libffi库来调用对应的JNI方法,以屏蔽Dalvik虚拟机运行在不同目标平台的细节。

至此,我们就分析完成Dalvik虚拟机JNI方法的注册过程了。这样,我们就打通了Java代码和Native代码之间的道路。实际上,很多Java和Android核心类的功能都是通过本地操作系统提供的系统调用来完成的,例如,Zygote类的成员函数forkAndSpecialize最终是通过Linux系统调用fork来创建一个Android应用程序进程的,又如,Thread类的成员函数start最终是通过pthread线程库函数pthread_create来创建一个Android应用程序线程的。

在接下来的一篇文章中,我们就在Java代码和Native代码打通了的基础上,分析Android应用程序进程和线程与本地操作系统进程的线程的关系,也就是Dalvik虚拟机进程和线程与本地操作系统进程的线程的关系,敬请关注!

分享到:
评论

相关推荐

    Dalvik虚拟机 PPT版

    Dalvik虚拟机与Java虚拟机共享有差不多的特性,例如,它们都是解释执行,并且支持即时编译(JIT)、垃圾收集(GC)、Java本地方法调用(JNI)和Java远程调试协议(JDWP)等,差别在于两者执行的指令集是不一样的,...

    Xposed框架原理深入研究

    Xposed框架的技术核心建立在Jvm原生的JNI机制之上,为了对Xposed框架进行深入分析,同时方便大家理解,我们从以下三个问题着手:1.Dalvik虚拟机在执行java层代码时如何识别JNI方法?2.怎样才能将java层普通方法注册...

    深入解析ANDROID虚拟机

    分别讲解了Android系统的基础知识、Android系统的结构和核心框架、Java虚拟机和Dalvik虚拟机的知识、实现程序编译和调试、Dalvik的运作流程、DEX优化和安全管理、Android虚拟机生命周期管理和内存分配策略、虚拟机...

    Android 深入研究JNI详解

    由于Android的应用层的类都是以Java写的,这些Java类编译为Dex型式的Bytecode之后,必须靠Dalvik虚拟机(VM: Virtual Machine)来执行。VM在Android平台里,扮演很重要的角色。 此外,在执行Java类的过程中,如果...

    Dalvik知识收集

    目录(导读) 一、Dalvik虚拟机工作原理介绍 二、Dalvik与Androi架构 三、JNI技术(java与dll交互的技术) 四、Dex文件结构 五、Apk文件结构、dex反编译 5.2 反编译工具Dedexer 六、JAVA虚拟机的结构解析

    Android NDK开发详细介绍

    Android之NDK开发  一、NDK产生的背景  Android平台从诞生起,就已经支持C、C++开发。众所周知,Android的SDK基于Java实现,这意味着... 不过,Google也表示,使用原生SDK编程相比Dalvik虚拟机也有一些劣势,Andro

    Android技术内幕.系统卷(扫描版)

    8.1.3 dalvik虚拟机与java虚拟机的区别 /465 8.2 dalvik构架与实现 /466 8.2.1 dalvik系统构架 /466 8.2.2 dx和dexdump工具 /468 8.2.3 .dex文件格式解析 /470 8.2.4 dalvik内部机制 /487 8.2.5 dalvik进程管理 /492...

    the latest CSS

    例如,可以先从Android的应用开发开始,等到对应用掌握的比较熟悉了,开始慢慢阅读一些Android 应用框架层的源代码,然后再渐渐往下去了解Android的JNI、Libraries、Dalvik虚拟机、HAL层、硬件驱动、Linux内核、ARM...

    中科大软院Android系统设计复习总结全

    Zygote 进程⼜称受精卵进程,它由 Init 进程的 app_process 启动,启动 Dalvik 虚拟机,加 载部分 Android 核⼼类及其 JNI ⽅法,Socket 的 Server 端可以接受创建应⽤请求。Zygote 在 启 动 的 过 程 中 创 建 ...

    Android技术内幕.系统卷 pdf

    8.1.3 dalvik虚拟机与java虚拟机的区别 /465 8.2 dalvik构架与实现 /466 8.2.1 dalvik系统构架 /466 8.2.2 dx和dexdump工具 /468 8.2.3 .dex文件格式解析 /470 8.2.4 dalvik内部机制 /487 8.2.5 dalvik进程...

    Android系统架构概述PPT

    应用程序框架进一步又分为C/C++和Java两个层次,Java代码运行Dalvik虚拟机之上,并且通过JNI方法和C/C++交互。应用程序层主要就是由四大组件Activity、Service、Broadcast Receiver和Content Provider构成,它们是...

    android底层开发

    android底层开发,介绍android架构,Activity,Manifest,INTENT,Service,Binder,Dalvik虚拟机,Framework,Linux,Navtive,JNI.....

    2010年谢彦的android笔记

    4.6 浅析dalvik虚拟机JIT技术的实现 133 4.7 应用程序的签名(Signature) 135 4.8 应用的权限 138 4.9 屏幕密度Density 140 4.10 Prelink实现的源码分析 142 4.11 适配硬件平台 145 4.12 其他介绍 147 4.12.1 手机...

    zxing.java源码解析-Android_Interview:Android_面试

    16.java虚拟机和Dalvik虚拟机的区别 17.线程sleep和wait有什么区别 18.View,ViewGroup事件分发 19.保存Activity状态 onSaveInstanceState() 7.http与https的区别 此处延伸:https的实现原理 23.自定义view和动画 ...

    【推荐】超全的移动安全自学资料精编合集(43份).zip

    Android安全开发基础: JVM Dalvik ART虚拟机 Android安全开发基础: Java本地接口(JNI) Android安全开发基础: Android系统的启动过程分析 Android安全开发基础: 图形界面(UI)和碎片(Fragment)(上) Android...

    如何定位AndroidNDK开发中遇到的错误

    众所周知,Android程序运行在Dalvik虚拟机中,NDK允许用户使用类似C/C++之类的原生代码语言执行部分程序。NDK包括:为何要用到NDK?概括来说主要分为以下几种情况:JavaNativeInterface(JNI)标准是Java平台的一...

    android笔记.rar

    4.6 浅析dalvik虚拟机JIT技术的实现... ...133 4.7 应用程序的签名(Signature) ... ...135 4.8 应用的权限... ..138 4.9 屏幕密度Density ... ..140 3 4.10 Prelink实现的源码分析 ... ...142 4.11 适配...

Global site tag (gtag.js) - Google Analytics