我们先来看看图1,它描述了ART运行时执行一个类方法的流程,如下所示:
图1 ART运行时执行类方法的过程
图1综合了我们在前面Android运行时ART加载OAT文件的过程分析和Android运行时ART加载类和方法的过程分析这两篇文章中提到的两个知识点,我们先来回顾一下。
第一个知识点是ART运行时将DEX字节码翻译成本地机器指令时,使用的后端(Backend)是Quick类型还是Portable类型。ART运行时在编译的时候,默认使用的后端是Quick类型的,不过可以通过将环境变量ART_USE_PORTABLE_COMPILER的值设置为true来指定使用Portable类型的后端,如下所示:
1 2 3 4 5 6 |
...... LIBART_CFLAGS := ifeq ($(ART_USE_PORTABLE_COMPILER),true) LIBART_CFLAGS += -DART_USE_PORTABLE_COMPILER=1 endif ...... |
上述编译脚本定义在文件art/runtime/Android.mk中。
一旦我们将环境变量ART_USE_PORTABLE_COMPILER的值设置为true,那么就会定义一个宏ART_USE_PORTABLE_COMPILER。参考前面Android运行时ART加载OAT文件的过程分析这篇文章,宏ART_USE_PORTABLE_COMPILER的定义与否会影响到加载OAT文件所使用的方法,如下所示:
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 |
OatFile* OatFile::Open(const std::string& filename, const std::string& location, byte* requested_base, bool executable) { CHECK(!filename.empty()) << location; CheckLocation(filename); #ifdef ART_USE_PORTABLE_COMPILER // If we are using PORTABLE, use dlopen to deal with relocations. // // We use our own ELF loader for Quick to deal with legacy apps that // open a generated dex file by name, remove the file, then open // another generated dex file with the same name. http://b/10614658 if (executable) { return OpenDlopen(filename, location, requested_base); } #endif // If we aren't trying to execute, we just use our own ElfFile loader for a couple reasons: // // On target, dlopen may fail when compiling due to selinux restrictions on installd. // // On host, dlopen is expected to fail when cross compiling, so fall back to OpenElfFile. // This won't work for portable runtime execution because it doesn't process relocations. UniquePtr<File> file(OS::OpenFileForReading(filename.c_str())); if (file.get() == NULL) { return NULL; } return OpenElfFile(file.get(), location, requested_base, false, executable); } |
这个函数定义在文件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运行时去,如下所示:
1 2 3 4 5 6 7 8 9 10 11 |
bool Runtime::Init(const Options& raw_options, bool ignore_unrecognized) { ...... java_vm_ = new JavaVMExt(this, options.get()); ...... Thread* self = Thread::Attach("main", false, NULL, false); ...... return true; } |
这个函数定义在文件art/runtime/runtime.cc中。
在Runtime类的成员函数Init中,通过调用Thread类的静态成员函数Attach将当前线程,也就是主线程,关联到ART运行时去。在关联的过程中,就会初始化一个外部库函数调用跳转表。
Thread类的静态成员函数Attach的实现如下所示:
1 2 3 4 5 6 /I0JBQkFCMA==/dissolve/70/gravity/SouthEast">
图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的实现如下所示: |