Android运行时ART执行类方法的过程分析

我们先来看看图1,它描述了ART运行时执行一个类方法的流程,如下所示:

图1 ART运行时执行类方法的过程

       图1综合了我们在前面Android运行时ART加载OAT文件的过程分析Android运行时ART加载类和方法的过程分析这两篇文章中提到的两个知识点,我们先来回顾一下。

第一个知识点是ART运行时将DEX字节码翻译成本地机器指令时,使用的后端(Backend)是Quick类型还是Portable类型。ART运行时在编译的时候,默认使用的后端是Quick类型的,不过可以通过将环境变量ART_USE_PORTABLE_COMPILER的值设置为true来指定使用Portable类型的后端,如下所示:

上述编译脚本定义在文件art/runtime/Android.mk中。

一旦我们将环境变量ART_USE_PORTABLE_COMPILER的值设置为true,那么就会定义一个宏ART_USE_PORTABLE_COMPILER。参考前面Android运行时ART加载OAT文件的过程分析这篇文章,宏ART_USE_PORTABLE_COMPILER的定义与否会影响到加载OAT文件所使用的方法,如下所示:

这个函数定义在文件art/runtime/oat_file.cc中。

这个函数的详细解释可以参考前面Android运行时ART加载OAT文件的过程分析一文,这里只对结论进行进一步的解释。从注释可以知道,通过Portable后端和Quick后端生成的OAT文件的本质区别在于,前者使用标准的动态链接器加载,而后者使用自定义的加载器加载。

标准动态链接器在加载SO文件(这里是OAT文件)的时候,会自动处理重定位问题。也就是说,在生成的本地机器指令中,如果有依赖其它的SO导出的函数,那么标准动态链接器就会将被依赖的SO也加载进来,并且从里面找到被引用的函数的地址,用来重定位引用了该函数的符号。生成的本地机器指令引用的一般都是些SO呢?其实就是ART运行时库(libart.so)。例如,如果在生成的本地机器指令需要分配一个对象,那么就需要调用ART运行时的堆管理器提供的AllocObject接口来分配。

自定义加载器的做法就不一样了。它在加载OAT文件时,并不需要做上述的重定位操作。因为Quick后端生成的本地机器指令需要调用一些外部库提供的函数时,是通过一个函数跳转表来实现的。由于在加载过程中不需要执行重定位,因此加载过程就会更快,Quick的名字就是这样得来的。Portable后端生成的本地机器指令在调用外部库提供的函数时,使用了标准的方法,因此它不但可以在ART运行时加载,也可以在其它运行时加载,因此就得名于Portable。

接下来我们的重点是分析Quick后端生成的本地机器指令在调用外部库函数时所使用的函数跳转表是如何定义的。

在前面分析Dalvik虚拟机的文章Dalvik虚拟机进程和线程的创建过程分析中,我们提到每一个Dalvik虚拟机线程在内部都通过一个Thread对象描述。这个Thread对象包含了一些与虚拟机相关的信息。例如,JNI函数调用函数表。在ART运行时中创建的线程,和Davik虚拟机线程一样,在内部也会通过一个Thread对象来描述。这个新的Thread对象内部除了定义JNI调用函数表之外,还定义了我们在上面提到的外部函数调用跳转表。

在前面Android运行时ART加载OAT文件的过程分析一文,我们提到了ART运行时的启动和初始化过程。其中的一个初始化过程便是将主线程关联到ART运行时去,如下所示:

这个函数定义在文件art/runtime/runtime.cc中。

在Runtime类的成员函数Init中,通过调用Thread类的静态成员函数Attach将当前线程,也就是主线程,关联到ART运行时去。在关联的过程中,就会初始化一个外部库函数调用跳转表。

Thread类的静态成员函数Attach的实现如下所示:

这个函数定义在文件art/runtime/thread.cc中。

在Thread类的静态成员函数Attach中,最重要的就是创建了一个Thread对象来描述当前被关联到ART运行时的线程。创建了这个Thread对象之后,马上就调用它的成员函数Init来对它进行初始化。

Thread类的成员函数Init的实现如下所示:

这个函数定义在文件art/runtime/thread.cc中。

Thread类的成员函数Init除了给当前的线程创建一个JNIEnvExt对象来描述它的JNI调用接口之外,还通过调用另外一个成员函数InitTlsEntryPoints来初始化一个外部库函数调用跳转表。

Thread类的成员函数InitTlsEntryPoints的实现如下所示:

这个函数定义在文件art/runtime/thread.cc中。

Thread类定义了四个成员变量interpreter_entrypoints_、jni_entrypoints_、portable_entrypoints_和quick_entrypoints_,如下所示:

Thread类的声明定义在文件art/runtime/thread.h中。

Thread类将外部库函数调用跳转表划分为4个,其中,interpreter_entrypoints_描述的是解释器要用到的跳转表,jni_entrypoints_描述的是JNI调用相关的跳转表,portable_entrypoints_描述的是Portable后端生成的本地机器指令要用到的跳转表,而quick_entrypoints_描述的是Quick后端生成的本地机器指令要用到的跳转表。从这里可以看出,Portable后端生成的本地机器指令也会使用到ART运行时内部的函数跳转表。不过与Quick后端生成的本地机器指令使用到的ART运行时内部的函数跳转表相比,它里面包含的函数项会少很多很多。接下来我们将会看到这一点。

回到Thread类的成员函数InitTlsEntryPoints中,它通过调用一个全局函数InitEntryPoints来初始化上述的4个跳转表。全局函数InitEntryPoints的实现是和CPU体系结构相关的,因为跳转表里面的函数调用入口是用汇编语言来实现的。

我们以ARM体系架构为例,来看全局函数InitEntryPoints的实现,如下所示:

这个函数定义在文件art/runtime/arch/arm/entrypoints_init_arm.cc中。

从函数InitEntryPoints的实现就可以看到Quick后端和Portable后端生成的本地机器指令要使用到的外部库函数调用跳转表的初始化过程了。例如,如果在生成的本地机器指令中,需要调用一个JNI函数,那么就需要通过art_jni_dlsym_lookup_stub函数来间接地调用,以便可以找到正确的JNI函数来调用。

此外,我们还可以看到,解释器要用到的跳转表只包含了两项,分别是artInterpreterToInterpreterBridge和artInterpreterToCompiledCodeBridge。前者用来从一个解释执行的类方法跳到另外一个也是解释执行的类方法去执行,后者用来从一个解释执行的类方法跳到另外一个以本地机器指令执行的类方法去执行。

Portable后端生成的本地机器指令要用到的跳转表也只包含了两项,分别是art_portable_resolution_trampoline和art_portable_to_interpreter_bridge。前者用作一个还未链接好的类方法的调用入口点,后者用来从一个以本地机器指令执行的类方法跳到另外一个解释执行的类方法去执行。

剩下的其它代码均是用来初始化Quick后端生成的本地机器指令要用到的跳转表,它包含的项非常多,但是可以划分为Alloc(对象分配)、Cast(类型转换)、DexCache(Dex缓访问)、Field(成员变量访问)、FillArray(数组填充)、JNI(JNI函数调用)、Locks(锁)、Math(数学计算)、Intrinsics(内建函数调用)、Invocation(类方法调用)、Thread(线程操作)和Throws(异常处理)等12类。

有了这些跳转表之后,当我们需要在生成的本地机器指令中调用一个外部库提供的函数时,只要找到用来描述当前线程的Thread对象,然后再根据上述的四个跳转表在该Thread对象内的偏移位置,那么就很容易找到所需要的跳转项了。

以上就是我们需要了解的第一个知识点,也就是Portable后端和Quick后端生成的本地机器指令的区别。接下来我们现来看另外一个知识点,它们是涉及到类方法的执行方式,也就是是通过解释器来执行,还是直接以本地机器指令来执行,以及它们之间是如何穿插执行的。

在前面Android运行时ART加载类和方法的过程分析这篇文章中,我们提到,在类的加载过程中,需要对类的各个方法进行链接,实际上就是确定它们是通过解释器来执行,还是以本地机器指令来直接执行,如下所示:

这个函数定义在文件art/runtime/class_linker.cc中。

函数LinkCode的详细解释可以参考前面Android运行时ART加载类和方法的过程分析一文,这里我们只对结论进行总结,以及对结论进行进一步的分析:

1. ART运行时有两种执行方法:解释执行模式和本地机器指令执行模式。默认是本地机器指令执行模式,但是在启动ART运行时时可以通过-Xint选项指定为解释执行模式。

2. 即使是在本地机器指令模式中,也有类方法可能需要以解释模式执行。反之亦然。解释执行的类方法通过函数artInterpreterToCompiledCodeBridge的返回值调用本地机器指令执行的类方法;本地机器指令执行的类方法通过函数GetCompiledCodeToInterpreterBridge的返回值调用解释执行的类方法;解释执行的类方法通过函数artInterpreterToInterpreterBridge的返回值解释执行的类方法。

3. 在解释执行模式下,除了JNI方法和动态Proxy方法,其余所有的方法均通过解释器执行,它们的入口点设置为函数GetCompiledCodeToInterpreterBridge的返回值。

4. 无论是哪一种执行模式,抽象方法都是解释执行,这是由于需要通过解释器来确定要执行的具体子类方法。它们的入口点同样是设置为函数GetCompiledCodeToInterpreterBridge的返回值。

5. 静态类方法的执行模式延迟至类初始化确定。在类初始化之前,它们的入口点由函数GetResolutionTrampoline的返回值代理。

接下来,我们就着重分析artInterpreterToCompiledCodeBridge、GetCompiledCodeToInterpreterBridge、artInterpreterToInterpreterBridge和GetResolutionTrampoline这4个函数以及它们所返回的函数的实现,以便可以更好地理解上述5个结论。

函数artInterpreterToCompiledCodeBridge用来在解释器中调用以本地机器指令执行的函数,它的实现如下所示:

这个函数定义在文件art/runtime/entrypoints/interpreter/interpreter_entrypoints.cc中。

被调用的类方法通过一个ArtMethod对象来描述,并且可以在调用栈帧shadow_frame中获得。获得了用来描述被调用方法的ArtMehtod对象之后,就可以调用它的成员函数Invoke来对它进行执行。后面我们就会看到,ArtMethod类的成员函数Invoke会找到类方法的本地机器指令来执行。

在调用类方法的本地机器指令的时候,从解释器调用栈获取的传入参数根据ART运行时使用的是Quick后端还是Portable后端来生成本地机器指令有所不同。不过最终都会ArtMethod类的成员函数Invoke来执行被调用类方法的本地机器指令。

函数GetCompiledCodeToInterpreterBridge用来返回一个函数指针,这个函数指针指向的函数用来从以本地机器指令执行的类方法中调用以解释执行的类方法,它的实现如下所示:

这个函数定义在文件art/runtime/entrypoints/entrypoint_utils.h中。

根据ART运行时使用的是Quick后端还是Portable后端,函数GetCompiledCodeToInterpreterBridge的返回值有所不同,不过它们的作用是一样的。我们假设ART运行时使用的是Quick后端,那么函数GetCompiledCodeToInterpreterBridge的返回值通过调用函数GetQuickToInterpreterBridge来获得。

函数GetQuickToInterpreterBridge的实现如下所示:

这个函数定义在文件art/runtime/entrypoints/entrypoint_utils.h中。

函数GetQuickToInterpreterBridge的返回值实际上指向的是函数art_quick_to_interpreter_bridge。函数art_quick_to_interpreter_bridge是使用汇编代码来实现的,用来从本地机器指令进入到解释器的。

以ARM体系结构为例,函数art_quick_to_interpreter_bridge的实现如下所示:

这个函数定义在文件art/runtime/arch/arm/quick_entrypoints_arm.S中。

很明显,函数art_quick_to_interpreter_bridge通过调用另外一个函数artQuickToInterpreterBridge从本地机器指令进入到解释器中去。

函数artQuickToInterpreterBridge的实现如下所示:

这个函数定义在文件art/runtime/entrypoints/quick/quick_trampoline_entrypoints.cc中。

函数artQuickToInterpreterBridge的作用实际上就是找到被调用类方法method的DEX字节码code_item,然后根据调用传入的参数构造一个解释器调用栈帧shadow_frame,最后就可以通过函数interpreter::EnterInterpreterFromStub进入到解释器去执行了。

既然已经知道了要执行的类方法的DEX字节码,以及已经构造好了要执行的类方法的调用栈帧,我们就不难理解解释器是如何执行该类方法了,具体可以参考一下Dalvik虚拟机的运行过程分析这篇文章描述的Dalvik虚拟机解释器的实现。

如果要执行的类方法method是一个静态方法,那么我们就需要确保它的声明类是已经初始化过了的。如果还没有初始化过,那么就需要调用ClassLinker类的成员函数EnsureInitialized来对它进行初始化。

函数artInterpreterToInterpreterBridge用来从解释执行的函数调用到另外一个也是解释执行的函数,它的实现如下所示:

这个函数定义在文件art/runtime/interpreter/interpreter.cc中。

对比函数artInterpreterToInterpreterBridge和artQuickToInterpreterBridge的实现就可以看出,虽然都是要跳入到解释器去执行一个被调用类方法,但是两者的实现是不一样的。前者由于调用方法本来就是在解释器中执行的,因此,调用被调用类方法所需要的解释器栈帧实际上已经准备就绪,并且被调用方法的DEX字节码也已经知晓,因此这时候就可以直接调用另外一个函数Execute来继续在解释器中执行。

同样,如果被调用的类方法是一个静态方法,并且它的声明类还没有被初始化,那么就需要调用ClassLinker类的成员函数EnsureInitialized来确保它的声明类是已经初始化好了的。

如果被调用的类方法是一个JNI方法,那么此种情况在ART运行时已经启动之后不允许的(ART运行时启动之前允许,但是只是测试ART运行时时才会用到),因为JNI方法在解释器中有自己的调用方式,而函数函数artInterpreterToInterpreterBridge仅仅是用于调用非JNI方法,因此这时候就会调用另外一个函数UnstartedRuntimeJni记录和抛出错误。

函数GetResolutionTrampoline用来获得一个延迟链接类方法的函数。这个延迟链接类方法的函数用作那些在类加载时还没有链接好的方法的调用入口点,也就是还没有确定调用入口的类方法。对于已经链接好的类方法来说,无论它是解释执行,还是本地机器指令执行,相应的调用入口都是已经通过ArtMehtod类的成员函数SetEntryPointFromCompiledCode和SetEntryPointFromInterpreter设置好了的。如上所述,这类典型的类方法就是静态方法,它们需要等到类初始化的时候才会进行链接。

函数GetResolutionTrampoline的实现如下所示:

这个函数定义在文件art/runtime/entrypoints/entrypoint_utils.h中。

我们假设没有定义宏ART_USE_PORTABLE_COMPILER,那么接下来就会调用GetQuickResolutionTrampoline来获得一个函数指针。

函数GetQuickResolutionTrampoline的实现如下所示:

这个函数定义在文件art/runtime/entrypoints/entrypoint_utils.h中。

函数GetQuickResolutionTrampoline又是通过调用参数class_linker指向的ClassLinker对象的成员函数GetQuickResolutionTrampoline来获得一个函数指针的。

ClassLinker类的成员函数GetQuickResolutionTrampoline的实现如下所示:

这个函数定义在文件art/runtime/class_linker.h中。

ClassLinker类的成员函数GetQuickResolutionTrampoline返回的是成员变量quick_resolution_trampoline_的值。那么ClassLinker类的成员变量quick_resolution_trampoline_的值是什么时候初始化的呢?是在ClassLinker类的成员函数InitFromImage中初始化。如下所示:

这个函数定义在文件art/runtime/class_linker.h中。

从前面Android运行时ART加载OAT文件的过程分析这篇文章可以知道,ART运行时在启动的时候,会加载一个Image文件,并且根据这个Image文件创建一个Image空间。这个Image空间属于ART运行时堆的一部分。后面我们分析ART运行时的垃圾收集机制再详细分析。

Image文件是ART运行时第一次启动时翻译系统启动类的DEX字节码创建的OAT文件的过程中创建的。我们将这个OAT文件称为启动OAT文件。这个启动OAT文件的OAT头部包含有一个quick_resolution_trampoline_offset_字段。这个quick_resolution_trampoline_offset_字段指向一小段Trampoline代码。这一小段Trampoline代码的作用是找到当前线程类型为Quick的函数跳转表中的pQuickResolutionTrampoline项,并且跳到这个pQuickResolutionTrampoline项指向的函数去执行。

从上面的分析可以知道,类型为Quick的函数跳转表中的pQuickResolutionTrampoline项指向的函数为art_quick_resolution_trampoline,它是一个用汇编语言实现的函数,如下所示:

这个函数定义在文件art/runtime/arch/arm/quick_entrypoints_arm.S中。

函数art_quick_resolution_trampoline首先是调用另外一个函数artQuickResolutionTrampoline来获得真正要调用的函数的地址,并且通过bx指令跳到该地址去执行。函数artQuickResolutionTrampoline的作用就是用来延迟链接类方法的,也就是等到该类方法被调用时才会对它进行解析链接,确定真正要调用的函数。

函数artQuickResolutionTrampoline的实现如下所示:

这个函数定义在文件art/runtime/entrypoints/quick/quick_trampoline_entrypoints.cc中。

第一个参数called表示被调用的类方法,第二个参数receiver表示被调用的对象,也就是接收消息的对象,第三个参数thread表示当前线程,第四个参数sp指向调用栈顶。通过调用QuickArgumentVisitor类的静态成员函数GetCallingMethod可以在调用栈找到类方法called的调用者,保存在变量caller中。

被调用类方法called有可能是一个运行时方法(Runtime Method)。运行时方法相当是一个替身,它是用来找到被替换的类方法。当调用类方法called是一个运行时方法时,调用它的成员函数IsRuntimeMethod得到的返回值为true,这时候我们就需要找到被替换的类方法。那么问题就来了,怎么找到此时被替换的类方法呢?运行时方法只是一个空壳,没有任何线索可以提供给我们,不过我们却可以在DEX字节码的调用指令中找到一些蜘丝马迹。在DEX字节码中,我们在一个类方法中通过invoke-static/invoke-direct/invoke-interface/invoke-super/invoke-virtual等指令来调用另外一个类方法。在这些调用指令中,有一个寄存器记录了被调用的类方法在DEX文件中的方法索引dex_method_index。有了这个DEX文件方法索引之后,我们就可以在相应的DEX文件找到被替换的类方法了。现在第二个问题又来了,我们要在哪一个DEX文件查找被替换的类方法呢?函数artQuickResolutionTrampoline适用的是调用方法caller和被调用方法called均是位于同一个DEX文件的情况。因此,我们可以通过调用方法caller来得到要查找的DEX文件dex_file。有了上述两个重要的信息之后,函数artQuickResolutionTrampoline接下来就可以调用ClassLinker类的成员函数ResolveMethod来查找被替换的类方法了,并且继续保存在参数called中。另一方面,如果被调用类方法called不是运行时方法,那么情况就简单多了,因为此时called描述的便是要调用的类方法。

经过上面的处理之后,参数called指向的ArtMethod对象还不一定是最终要调用的类方法。这是因为当前发生的可能是一个虚函数调用或者接口调用。在上述两种情况下,我们需要通过接收消息的对象receiver来确定真正被调用的类方法。为了完成这个任务,我们首先通过调用Object类的成员函数GetClass获得接收消息的对象receiver的类对象,接着再通过调用过Class类的成员函数FindVirtualMethodForVirtual或者FindVirtualMethodForInterface来获得真正要被调用的类方法。前者针对的是虚函数调用,而后者针对的是接口调用。

最终我们得到的真正被调用的类方法仍然是保存在参数called中。这时候事情还没完,因为此时被调用的类方法所属的类可能还没有初始化好。因此,在继续下一步操作之前,我们需要调用ClassLinker类的成员函数EnsureInitialized来确保存被调用类方法called所属的类已经初始好了。在调用ClassLinker类的成员函数EnsureInitialized的时候,如果被调用类方法called所属的类还没有初始化,那么就会对它进行初始化,不过不等它初始化完成就返回了。因此,这时候就可能会出现两种情况。

第一种情况是被调用类方法called所属的类已经初始好了。这时候我们就可以直接调用它的成员函数GetEntryPointFromCompiledCode来获得它的本地机器指令或者DEX字节码,取决于它是以本地机器指令方式执行还是通过解释器来执行。

第二种情况是被调用方法called所属的类正在初始化中。这时候需要区分静态和非静态调用两种情况。在进一步解释之前,我们需要明确,类加载和类初始化是两个不同的操作。类加载的过程并不一定会伴随着类的初始化。此时我们唯一确定的是被调用方法called所属的类已经被加载(否则它的类方法无法被调用)。又从前面Android运行时ART加载类和方法的过程分析这篇文章可以知道,当一个类被加载时,除了它的静态成员函数,其余所有的成员函数均已加载完毕。这意味着我们可以直接调用ArtMethod类的成员函数GetEntryPointFromCompiledCode来获得被调用方法called的本地机器指令或者DEX字节码。对于静态成员函数的情况,我们就唯有到DEX文件去查找到被调用方法called的本地机器指令了。这是通过调用ClassLinker类的成员函数GetOatCodeFor来实现的。当然,如果该静态成员函数不存在本地机器指令,那么ClassLinker类的成员函数GetOatCodeFor返回的是进入解释器的入口函数地址。这样我们就可以通过解释器来执行该静态成员函数了。

最后,函数artQuickResolutionTrampoline将获得的真正被调用的类方法的执行入口地址code返回给前一个函数,即art_quick_resolution_trampoline,以便后者可以通过bx跳过去执行。函数artQuickResolutionTrampoline在返回之前,同时还会将此时栈顶的内容设置为真正被调用的类方法对象,以便真正被调用的类方法在运行时,可以获得正确的调用栈帧。

到这里,函数artQuickResolutionTrampoline的实现就分析完成了。不过对于上面提到的运行时方法,我们还需要继续解释。只有了理解了运行时方法的作用之后,我们才能真正理解函数artQuickResolutionTrampoline的作用。

运行时方法与另一个称为Dex Cache的机制有关。在ART运行时中,每一个DEX文件都有一个关联的Dex Cache,用来缓存对应的DEX文件中已经被解析过的信息,例如类方法和类属性等。这个Dex Cache使用类DexCache来描述,它的定义如下所示:

这个类定义在文件rt/runtime/mirror/dex_cache.h中。

这里我们只关注Dex Cache中的类方法,它通过成员变量resolved_methods_来描述。

在ART运行时中,每当一个类被加载时,ART运行时都会检查该类所属的DEX文件是否已经关联有一个Dex Cache。如果还没有关联,那么就会创建一个Dex Cache,并且建立好关联关系。以Java层的DexFile类为例,当我们通过调用它的成员函数loadClass来加载一个类的时候,最终会调用到C++层的JNI函数DexFile_defineClassNative来执行真正的加载操作。

函数DexFile_defineClassNative的实现如下所示:

这个函数定义在文件art/runtime/native/dalvik_system_DexFile.cc中。

参数cookie指向之前已经打开了的DEX文件,因此这里首先将它转换为一个DexFile指针dex_file。这个DEX文件是包含在OAT文件里面的,它们的打开过程可以参考Android运行时ART加载OAT文件的过程分析一文。得到了之前打开的DEX文件之后,接下来就调用ClassLinker类的成员函数RegisterDexFile将它注册到ART运行时中去,以便以后可以查询使用。最后再通过ClassLinker类的成员函数DefineClass来加载参数javaName指定的类。

类的加载过程可以参考前面Android运行时ART加载类和方法的过程分析一文,接下来我们主要关注ClassLinker类的成员函数RegisterDexFile的实现,如下所示:

这个函数定义在文件art/runtime/class_linker.cc中。

ClassLinker类的成员函数RegisterDexFile首先将调用另外一个成员函数IsDexFileRegisteredLocked检查参数dex_file指定的DEX文件是否已经注册过了。如果已经注册过了,那么就什么也不用做。否则的话,就调用ClassLinker类的成员函数AllocDexCache为其分配一个Dex Cache,并且调用ClassLinker类的成员函数RegisterDexFileLocked执行真正的注册工作。

从上面的分析就可以看出,注册DEX文件实际上就是创建一个与之关联的Dex Cache,并且将该Dex Cache保存在ClassLinker类的成员变量dex_caches_所描述的一个向量中。不过,这里我们关注的是注册过程中所创建的Dex Cache。因此,接下来我们继续分析ClassLinker类的成员函数AllocDexCache的实现,如下所示:

这个函数定义在文件art/runtime/class_linker.cc中。

我们要创建的Dex Cache使用java.lang.DexCache类来描述。java.lang.DexCache类对象保存在ART运行时的Image空间中,我们可以通过ClassLinker类的成员函数GetClassRoot来获得的。获得了用来描述java.lang.DexCache类的类对象之后,我们就可以调用Heap类的成员函数AllocObject在ART运行堆上分配一个DexCache对象了。关于ART运行时的Image空间和堆,我们接下来的文章中将会详细分析。

Dex Cache的作用是用来缓存包含在一个DEX文件里面的类型(Type)、方法(Method)、域(Field)、字符串(String)和静态储存区(Static Storage)等信息。因此,我们需要为上述提到的信息分配储存空间。例如,对于类方法来说,我们需要创建一个ArtMethod对象指针数组,其中每一个ArtMethod对象都用来描述DEX文件里面的一个类方法。有了这些储存空间之后,最后就可以调用DexCache类的成员函数Init对刚才创建的Dex Cache进行初始化了。

DexCache类的成员函数Init的实现如下所示:

这个函数定义在文件art/runtime/mirror/dex_cache.cc中。

我们重点关注Dex Cache中的类方法对象数组的初始化。前面提到,DexCache类有一个类型为ObjectArray<ArtMethod>的resolved_methods_指针数组。我们通过DexCache类的成员函数ResolvedMethodsOffset可以知道它在DexCache类中的偏移值。有了这个偏移值之后,就可以调用父类Object的成员函数SetFieldObject来将参数resolved_methods描述的ArtMethod对象指针数组设置到当前正在初始化的DexCache对象的成员变量resolved_methods_去了。

接下来重点就来了,DexCache类的成员变量resolved_methods_指向的ArtMethod对象指针数组中的每一个ArtMethod指针都会指向同一个Resolution Method。这个Resolution Method可以通过Runtime类的成员函数GetResolutionMethod获得,它的实现如下所示:

这个函数定义在文件rt/runtime/runtime.h中。

Runtime类的成员函数GetResolutionMethod返回的是成员变量resolution_method_指向的一个ArtMethod对象。

那么Runtime类的成员变量resolution_method_是什么时候初始化的呢?是在ART运行时的Image空间初始化过程中初始化的。在前面Android运行时ART加载OAT文件的过程分析一篇文章中,我们提到,ART运行时的Image空间创建完成之后,会调用ImageSpace类的成员函数Init来对它进行初始化。

ImageSpace类的成员函数Init的实现如下所示:

这个函数定义在文件art/runtime/gc/space/image_space.cc中。

ImageSpace类的成员函数Init首先是将Image文件的头部读取出来,并且根据得到的Image头部信息将Image文件映射到指定的内存位置。Image头部指定了ART运行时使用的Resolution Method在内存中的位置,可以通过ImageHeader类的成员函数GetImageRoot来获得。获得了这个Resolution Method之后,就可以调用Runtime类的成员函数SetResolutionMethod将它设置到ART运行时去了。

前面说了那么多,好像还是没有发现为什么要给ART运行时设置一个Resolution Method。迷局就要准备解开了。在解开之前,我们首先要知道ART运行时使用的Resolution Method是长什么样的,也就是它是如何创建的。

Resolution Method本质上就一个ArtMethod对象。当我们调用dex2oat工具将系统启动类翻译成本地机器指令时,会创建这个Resolution Method,并且将它保存在Image文件中。这样以后要使用这个Resolution Method时,就可以将对应的Image文件加载到内存获得。

Resolution Method是通过调用Runtime类的成员函数CreateResolutionMethod来创建的,如下所示:

这个函数定义在文件art/runtime/runtime.cc中。

从Runtime类的成员函数CreateResolutionMethod的实现就可以看出,ART运行时的Resolution Method有以下两个特点:

1. 它的Dex Method Index为DexFile::kDexNoIndex,这是因为它不代表任何的类方法。

2. 由于上述原因,它没有相应的本地机器指令,因此它不能执行。

回想我们前面分析的函数artQuickResolutionTrampoline,它通过ArtMethod类的成员函数IsRuntimeMethod来判断一个ArtMethod对象是否是一个运行时方法。ArtMethod类的成员函数IsRuntimeMethod的实现如下所示:

这个函数定义在文件art/runtime/mirror/art_method-inl.h文件中。

如果一个ArtMethod的Dex Method Index等于DexFile::kDexNoIndex,那么它就被认为是运行时方法。当然,Resoultion Method只是运行方法的其中一种。其中类型的运行时方法我们后面分析ART运行时的Image空间的文章时再讲解。

如前面所述,函数artQuickResolutionTrampoline一旦发现一个接着要调用的类方法是一个运行时方法时,就会调用ClassLinker类的成员函数ResolveMethod来对其进行解析,也就是找到真正要被调用的类方法。

ClassLinker类的成员函数ResolveMethod的实现如下所示:

这个函数定义在文件art/runtime/class_linker-inl.h中。

参数method_idx描述的是接下来将要被调用类方法在DEX文件的索引。注意,每一个类方法在宿主类中有一个索引,在对应的DEX文件中也有一个索引。这两个索引是不一样的,根据前面Android运行时ART加载类和方法的过程分析一文,前一个索引用来查找一个类方法的本地机器指令。而后面一个索引,自然的就是用来DEX文件中找到对应的类方法描述信息了。这意味着一旦知道一个类方法在DEX文件的索引,那么就可以在对应的DEX文件中对该类方法进行解析了。一旦解析完成,自然就可以知道接下来要被调用的类方法是什么了。

参数referrer指向的ArtMethod对象描述的是调用者(类方法)。每一个类方法都关联有一个ArtMethod对象指针数组,这个ArtMethod对象指针数组实际上就是我们在前面提到的Dex Cache中的ArtMethod对象指针数组。同时,每一个类对象(Class)也关联有一个Dex Cache。这个Dex Cache实际上就是与包含该类的DEX文件相关联的Dex Cache。为了搞清楚上述关系,我们回顾一下前面Android运行时ART加载类和方法的过程分析一文提到的ClassLinker类的两个成员函数DefineClass和LoadMethod。

在ClassLinker类的成员函数DefineClass中,会给每一个加载的类关联一个Dex Cache,如下所示:

这个函数定义在文件art/runtime/class_linker.cc中。

变量klass描述的就是正在加载的类,在对其进行加载之前,首先会调用ClassLinker类的成员函数FindDexCache找到与参数dex_file描述的DEX文件相关联的Dex Cache。有了这个Dex Cache,就可以将它设置到kclass指向的Class对象中去了。注意,参数dex_file描述的DEX文件就是包含正在加载的类的文件。

在ClassLinker类的成员函数LoadMethod中,会给每一个加载的类方法设置一个DEX文件类方法索引,以及关联一个ArtMethod对象指针数组,如下所示:

这个函数定义在文件art/runtime/class_linker.cc中。

变量dst描述的便是正在加载的类方法,我们可以通过参数it获得它在DEX文件中的类方法索引,并且将该索引设置到变量dst指向的ArtMethod对象中去。

参数klass描述是正在加载的类方法所属的类,前面我们已经给这个类关联过一个Dex Cache了,因此,只要将重新获得该Dex Cache,并且获得该Dex Cache里面的ArtMethod对象指针数组,那么就可以将ArtMethod对象指针数组设置到正在加载的类方法去了。

从ClassLinker类的两个成员函数DefineClass和LoadMethod的实现就可以看出,同一个DEX文件的所有类关联的Dex Cache都是同一个Dex Cache,并且属于这些类的所有类方法关联的ArtMethod对象指针数组都是该Dex Cache内部的ArtMethod对象指针数组。这个结论对我们理解ClassLinker类的成员函数ResolveMethod的实现很重要。

在ClassLinker类的成员函数ResolveMethod中,我们知道的是调用者以及被调用者在DEX文件中的类方法索引,因此,我们就可以从与调用者关联的ArtMethod对象指针数组中找到接下来真正要被调用的类方法了。

Dex Cache内部的ArtMethod对象指针数组的每一个ArtMethod指针一开始都是指向ART运行时的Resolution Method。但是每当一个类方法第一次被调用的时候,函数artQuickResolutionTrampoline能够根据捕捉到这种情况,并且根据调用者和调用指令的信息,通过ClassLinker类的成员函数ResolveMethod找到接下来真正要被调用的类方法。查找的过程就是解析类方法的过程,这是一个漫长的过程,因为要解析DEX文件。不过一旦接下来要被调用的类方法解析完成,就会创建另外一个ArtMethod对象来描述解析得到的信息,并且将该ArtMethod对象保存在对应的Dex Cache内部的ArtMethod对象指针数组的相应位置去。这样下次该类方法再被调用时,就不用再次解析了。

从上面的分析我们还可以进一步得到以下的结论:

1. 在生成的本地机器指令中,一个类方法调用另外一个类方法并不是直接进行的,而是通过Dex Cache来间接进行的。

2. 通过Dex Cache间接调用类方法,可以做到延时解析类方法,也就是等到类方法第一次被调用时才解析,这样可以避免解析那些永远不会被调用的类方法。

3. 一个类方法只会被解析一次,解析的结果保存在Dex Cache中,因此当该类方法再次被调用时,就可以直接从Dex Cache中获得所需要的信息。

以上就是Dex Cache在ART运行时所起到的作用了,理解这一点对阅读ART运行时的源代码非常重要。

有了以上的知识点之后,接下来我们就可以真正地分析类方法的调用过程了。在Android运行时ART加载类和方法的过程分析一文中,我们通过AndroidRuntime类的成员函数start来分析类和类方法的加载过程。本文同样是从这个函数开始分析类方法的执行过程,如下所示:

这个函数定义在文件frameworks/base/core/jni/AndroidRuntime.cpp。

找到要调用类方法之后,就可以调用JNI接口CallStaticVoidMethod来执行它了。

根据我们在Android运行时ART加载类和方法的过程分析一文的分析可以知道,JNI接口CallStaticVoidMethod由JNI类的成员函数CallStaticVoidMethod实现,如下所示:

这个函数定义在文件art/runtime/jni_internal.cc中。

JNI类的成员函数CallStaticVoidMethod实际上又是通过全局函数InvokeWithVarArgs来调用参数mid指定的方法的,如下所示:

这个函数定义在文件art/runtime/jni_internal.cc中。

函数InvokeWithVarArgs将调用参数封装在一个数组中,然后再调用另外一个函数InvokeWithArgArray来参数mid指定的方法。参数mid实际上是一个ArtMethod对象指针,因此,我们可以将它转换为一个ArtMethod指针,于是就可以得到被调用类方法的相关信息了。

函数InvokeWithArgArray的实现如下所示:

这个函数定义在文件art/runtime/jni_internal.cc中。

函数InvokeWithArgArray通过ArtMethod类的成员函数Invoke来调用参数method指定的类方法。

ArtMethod类的成员函数Invoke的实现如下所示:

这个函数定义在文件rt/runtime/mirror/art_method.cc中。

ArtMethod类的成员函数Invoke的执行逻辑如下所示:

1. 构造一个类型为ManagedStack的调用栈帧。这些调用栈帧会保存在当前线程对象的一个链表中,在进行垃圾收集会使用到。

2. 如果ART运行时还没有启动,那么这时候是不能够调用任何类方法的,因此就直接返回。否则,继续往下执行。

3. 从前面的函数LinkCode可以知道,无论一个类方法是通过解释器执行,还是直接以本地机器指令执行,均可以通过ArtMethod类的成员函数GetEntryPointFromCompiledCode获得其入口点,并且该入口不为NULL。不过,这里并没有直接调用该入口点,而是通过Stub来间接调用。这是因为我们需要设置一些特殊的寄存器。如果ART运行时使用的是Quick类型的后端,那么使用的Stub就为art_quick_invoke_stub。否则的话,使用的Stub就为art_portable_invoke_stub。

4. 如果在执行类方法的过程中,出现了一个值为-1的异常,那么就在运行生成的本地机器指令出现了问题,这时候就通过解释器来继续执行。每次通过解释器执行一个类方法的时候,都需要构造一个类型为ShadowFrame的调用栈帧。这些调用栈帧同样是在垃圾回收时使用到。

接下来我们主要是分析第3步,并且假设目标CPU体系架构为ARM,以及ART运行时使用的是Quick类型的后端,这样第3步使用的Stub就为函数art_quick_invoke_stub,它的实现如下所示:

这个函数定义在文件art/runtime/arch/arm/quick_entrypoints_arm.S中。

函数art_quick_invoke_stub前面的注释列出了 函数art_quick_invoke_stub被调用的时候,寄存器r0-r3的值,以及调用栈顶端的两个值。其中,r0指向当前被调用的类方法,r1指向一个参数数组地址,r2记录参数数组的大小,r3指向当前线程。调用栈顶端的两个元素分别用来保存调用结果及其类型。

无论一个类方法是通过解释器执行,还是直接以本地机器指令执行,当它被调用时,都有着特殊的调用约定。其中,寄存器r9指向用来描述当前调用线程的一个Thread对象地址,这样本地机器指令在执行的过程中,就可以通过它来定位线程的相关信息,例如我们在前面描述的各种函数跳转表;寄存器r4初始化为一个计数值,当计数值递减至0时,就需要检查当前线程是否已经被挂起;寄存器r0指向用来描述被调用类方法的一个ArtMethod对象地址。

所有传递给被调用方法的参数都会保存在调用栈中,因此,在进入类方法的入口点之前,需要在栈中预留足够的位置,并且通过调用memcpy函数将参数都拷贝到预留的栈位置去。同时,前面三个参数还会额外地保存在寄存器r1、r2和r3中。这样对于小于等于3个参数的类方法,就可以通过访问寄存器来快速地获得参数。

注意,传递给被调用类方法的参数并不是从栈顶第一个位置(一个位置等于一个字长,即4个字节)开始保存的,而是从第二个位置开始的,即sp + 4。这是因为栈顶的第一个位置是预留用来保存用来描述当调用类方法(Caller)的ArtMethod对象地址的。由于函数art_quick_invoke_stub是用来从外部进入到ART运行时的,即不存在调用类方法,因此这时候栈顶第一个位置会被设置为NULL。

准备好调用栈帧之后,就找到从用来描述当前调用类方法的ArtMethod对象地址偏移METHOD_CODE_OFFSET处的值,并且以该值作为类方法的执行入口点,最后通过blx指令跳过去执行。

METHOD_CODE_OFFSET的值定义在文件art/runtime/asm_support.h中,值为40,如下所示:

参照注释,可以知道,在ArtMethod对象中,偏移值为40的成员变量为entry_point_from_compiled_code_,而该成员变量就是调用函数LinkCode链接类方法时调用ArtMethod类的成员函数SetEntryPointFromCompiledCode设置的。相应的,可以通过ArtMethod类的成员函数GetEntryPointFromCompiledCode获得该成员变量的值,如前面ArtMethod类的成员函数Invoke所示。

在ARM体系架构中,当调用blx指令跳转到指定的地址执行,那么位于blx下面的一条指令会保存在寄存器lr中,而被调用类方法在执行前,一般又会通过SETUP_REF_AND_ARGS_CALLEE_SAVE_FRAME宏保存指定的寄存器。

宏SETUP_REF_AND_ARGS_CALLEE_SAVE_FRAME的定义如下所示:

这个宏定义在文件art/runtime/arch/arm/quick_entrypoints_arm.S中。

寄存器r1-r3、r5-r8、r10-r11和lr在栈上依次从低地址往高地址保存,即从栈顶开始保存。除了保存上述寄存器之外,还会在栈上预留两个位置(每个位置等于一个字长,即4个字节),其中,栈顶第一个位置用来保存用来描述当前被调用的类方法的ArtMethod对象地址,第二个位置可能是设计用来保存寄存器r0的,但是目前还没有发现这样使用的地方。宏SETUP_REF_AND_ARGS_CALLEE_SAVE_FRAME的使用情景之一可能参考我们前面分析的函数art_quick_resolution_trampoline。

现在还有一个问题是,上面提到的栈顶第一位置是什么时候会被设置为用来描述当前被调用的类方法的ArtMethod对象地址的呢?因为宏SETUP_REF_AND_ARGS_CALLEE_SAVE_FRAME只是在栈上预留这个位置,但是没有设置这个位置的值。以我们前面分析的函数artQuickResolutionTrampoline为例,一开始这个位置被函数FinishCalleeSaveFrameSetup设置为一个类型为Runtime::kRefsAndArgs的运行时方法,这是因为这时候我们还不知道真正被调用的类方法是什么。类型为Runtime::kRefsAndArgs的运行时方法与我们前面提到的Resolution Method的作用有点类似,虽然它们本身是一个ArtMethod对象,但是它们的真正作用是用来描述其它的ArtMethod。例如,类型为Runtime::kRefsAndArgs的运行时方法要描述的就是真正要被调用类的类方法会在栈上保存r1-r3、r5-r8、r10-r11和lr这10个寄器,并且会在栈上预留两个额外的位置,其中的一个位置用来保存真正被调用的类方法。果然,等到函数artQuickResolutionTrampoline找到真正被调用的类方法之后,就会将相应的ArtMethod对象地址保存在栈顶上。这正是到函数artQuickResolutionTrampoline返回前所做的事情。

综合上面的分析,我们就得到ART运行时类方法的调用约定,如下所示:

左边显示了一个类方法(Caller)调用另外一个类方法(Callee)时栈的内存布局情况,右边显示了负责安排这些内存布局每一部分的一个情景。

回到前面的函数art_quick_invoke_stub中,通过blx指令调用指定的类方法结束后,结果就保存在r0和r1两个寄存器中,其中一个表示返回值,另外一个表示返回值类型,最后通过strd指令将这两个寄存器的值拷贝到栈上指定的内存地址去,实际上就将调用结果返回给调用者指定的两个变量去。

这样,我们就分析完成类方法的执行过程了,也基本上解释上前面分析的函数LinkCode所涉及到关键函数,包括artInterpreterToCompiledCodeBridge、GetCompiledCodeToInterpreterBridge/GetQuickToInterpreterBridge/art_quick_to_interpreter_bridge/artQuickToInterpreterBridge、artInterpreterToInterpreterBridge、GetResolutionTrampoline/GetQuickResolutionTrampoline/GetQuickResolutionTrampoline/art_quick_resolution_trampoline/artQuickResolutionTrampoline、ArtMethod::SetEntryPointFromCompiledCode/ArtMethod::GetEntryPointFromCompiledCode和ArtMethod::SetEntryPointFromInterpreter等。

为了完整性,接下来我们继续分析一下与函数ArtMethod::SetEntryPointFromInterpreter相对应的函数ArtMethod::GetEntryPointFromInterpreter,以便可以看到由前者设置的函数入口点是在什么情况下被使用的。

前面提到,ArtMethod类的成员函数SetEntryPointFromInterpreter设置的入口点是用来给解释器调用另外一个类方法时使用的。ART运行时的解释器主要由art/runtime/interpreter/interpreter.cc文件中,当它需要调用另外一个类方法时,就会通过函数DoInvoke来实现,如下所示:

这个函数定义在art/runtime/interpreter/interpreter.cc文件中。

通过调用指令中的指定的Dex Method Index,我们可以通过另外一个函数FindMethodFromCode找到被调用的类方法,通过ArtMethod对象method来描述。有了这个ArtMethod对象后,我们就可以调用它的成员函数GetEntryPointFromInterpreter来获得接下来要被调用类方法的执行入口点。从函数LinkCode的实现可以知道,通过ArtMethod类的成员函数GetEntryPointFromInterpreter获得的类方法执行入口点有可能是用来进入解释器的,也有可能是用来进入到类方法的本地机器指令去的。

至此,我们就分析完成ART运行时执行一个类方法的过程以及在执行过程中涉及到的各种关键函数了。本文与其说是分析类方法的执行过程,不如说是分析ART运行时的实现原理。理解了本文分析到的各个关键函数之后,相信对ART运行时就会有一个清晰的认识了。在接下来的文章中,我们将继续发力,分析听起来很高大上的ART运行时垃圾收集机制。敬请关注!想了解更多信息,也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部