Android执行时ART载入OAT文件的过程分析
在前面一文中,我们介绍了Android执行时ART,它的核心是OAT文件。OAT文件是一种Android私有ELF文件格式,它不仅包括有从DEX文件翻译而来的本地机器指令。还包括有原来的DEX文件内容。这使得我们无需又一次编译原有的APK就能够让它正常地在ART里面执行。也就是我们不须要改变原来的APK编程接口。
本文我们通过OAT文件的载入过程分析OAT文件的结构,为后面分析ART的工作原理打基础。
老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!
《Android系统源码情景分析》一书正在进击的程序猿网(http://0xcc0xcd.com)中连载。点击进入。
OAT文件的结构如图1所看到的:
图1 OAT文件结构
因为OAT文件本质上是一个ELF文件,因此在最外层它具有一般ELF文件的结构,比如它有标准的ELF文件头以及通过段(Section)来描写叙述文件内容。
关于ELF文件的很多其他知识,能够參考维基百科:http://en.wikipedia.org/wiki/Executable_and_Linkable_Format。
作为Android私有的一种ELF文件,OAT文件包括有两个特殊的段oatdata和oatexec。前者包括实用来生成本地机器指令的dex文件内容,后者包括有生成的本地机器指令,它们之间的关系通过储存在oatdata段前面的oat头部描写叙述。此外,在OAT文件的dynamic段。导出了三个符号oatdata、oatexec和oatlastword,它们的值就是用来界定oatdata段和oatexec段的起止位置的。
当中,[oatdata, oatexec - 1]描写叙述的是oatdata段的起止位置。而[oatexec, oatlastword + 3]描写叙述的是oatexec的起止位置。要全然理解OAT的文件格式。除了要理解本文即将要分析的OAT载入过程之外。还须要掌握接下来文章分析的类和方法查找过程。
在分析OAT文件的载入过程之前,我们须要简介一下OAT是怎样产生的。如前面Android ART执行时无缝替换Dalvik虚拟机的过程分析一文所看到的,APK在安装的过程中。会通过dex2oat工具生成一个OAT文件:
static void run_dex2oat(int zip_fd, int oat_fd, const char* input_file_name,
const char* output_file_name, const char* dexopt_flags)
{
static const char* DEX2OAT_BIN = "/system/bin/dex2oat";
static const int MAX_INT_LEN = 12; // '-'+10dig+'\0' -OR- 0x+8dig
char zip_fd_arg[strlen("--zip-fd=") + MAX_INT_LEN];
char zip_location_arg[strlen("--zip-location=") + PKG_PATH_MAX];
char oat_fd_arg[strlen("--oat-fd=") + MAX_INT_LEN];
char oat_location_arg[strlen("--oat-name=") + PKG_PATH_MAX]; sprintf(zip_fd_arg, "--zip-fd=%d", zip_fd);
sprintf(zip_location_arg, "--zip-location=%s", input_file_name);
sprintf(oat_fd_arg, "--oat-fd=%d", oat_fd);
sprintf(oat_location_arg, "--oat-location=%s", output_file_name); ALOGV("Running %s in=%s out=%s\n", DEX2OAT_BIN, input_file_name, output_file_name);
execl(DEX2OAT_BIN, DEX2OAT_BIN,
zip_fd_arg, zip_location_arg,
oat_fd_arg, oat_location_arg,
(char*) NULL);
ALOGE("execl(%s) failed: %s\n", DEX2OAT_BIN, strerror(errno));
}
这个函数定义在文件frameworks/native/cmds/installd/commands.c中。
当中,參数zip_fd和oat_fd都是打开文件描写叙述符。指向的各自是正在安装的APK文件和要生成的OAT文件。OAT文件的生成过程主要就是涉及到将包括在APK里面的classes.dex文件的DEX字节码翻译成本地机器指令。这相当于是编写一个输入文件为DEX、输出文件为OAT的编译器。这个编译器是基于LLVM编译框架开发的。
编译器的工作原理比較高大上。所幸的是它不会影响到我们接下来的分析,因此我们就略过DEX字节码翻译成本地机器指令的过程,假设它非常愉快地完毕了。
APK安装过程中生成的OAT文件的输入仅仅有一个DEX文件,也就是来自于打包在要安装的APK文件中面的classes.dex文件。实际上,一个OAT文件是能够由若干个DEX生成的。这意味着在生成的OAT文件的oatdata段中,包括有多个DEX文件。那么。在什么情况下。会生成包括多个DEX文件的OAT文件呢?
从前面Android ART执行时无缝替换Dalvik虚拟机的过程分析一文能够知道。当我们选择了ART执行时时。Zygote进程在启动的过程中,会调用libart.so里面的函数JNI_CreateJavaVM来创建一个ART虚拟机。
函数JNI_CreateJavaVM的实现例如以下所看到的:
extern "C" jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
const JavaVMInitArgs* args = static_cast<JavaVMInitArgs*>(vm_args);
if (IsBadJniVersion(args->version)) {
LOG(ERROR) << "Bad JNI version passed to CreateJavaVM: " << args->version;
return JNI_EVERSION;
}
Runtime::Options options;
for (int i = 0; i < args->nOptions; ++i) {
JavaVMOption* option = &args->options[i];
options.push_back(std::make_pair(std::string(option->optionString), option->extraInfo));
}
bool ignore_unrecognized = args->ignoreUnrecognized;
if (!Runtime::Create(options, ignore_unrecognized)) {
return JNI_ERR;
}
Runtime* runtime = Runtime::Current();
bool started = runtime->Start();
if (!started) {
delete Thread::Current()->GetJniEnv();
delete runtime->GetJavaVM();
LOG(WARNING) << "CreateJavaVM failed";
return JNI_ERR;
}
*p_env = Thread::Current()->GetJniEnv();
*p_vm = runtime->GetJavaVM();
return JNI_OK;
}
这个函数定义在文件art/runtime/jni_internal.cc中。
參数vm_args用作ART虚拟机的启动參数,它被转换为一个JavaVMInitArgs对象后,再依照Key-Value的组织形式保存一个Options向量中。而且以该向量作为參数传递给Runtime类的静态成员函数Create。
Runtime类的静态成员函数Create负责在进程中创建一个ART虚拟机。创建成功后,就调用Runtime类的另外一个静态成员函数Start启动该ART虚拟机。注意,这个创建ART虚拟的动作仅仅会在Zygote进程中执行,SystemServer系统进程以及Android应用程序进程的ART虚拟机都是直接从Zygote进程fork出来共享的。这与Dalvik虚拟机的创建方式是全然一样的。
接下来我们就重点分析Runtime类的静态成员函数Create。它的实现例如以下所看到的:
bool Runtime::Create(const Options& options, bool ignore_unrecognized) {
// TODO: acquire a static mutex on Runtime to avoid racing.
if (Runtime::instance_ != NULL) {
return false;
}
InitLogging(NULL); // Calls Locks::Init() as a side effect.
instance_ = new Runtime;
if (!instance_->Init(options, ignore_unrecognized)) {
delete instance_;
instance_ = NULL;
return false;
}
return true;
}
这个函数定义在文件art/runtime/runtime.cc中。
instance_是Runtime类的静态成员变量。它指向进程中的一个Runtime单例。
这个Runtime单例描写叙述的就是当前进程的ART虚拟机实例。
函数首先推断当前进程是否已经创建有一个ART虚拟机实例了。假设有的话。函数就马上返回。否则的话。就创建一个ART虚拟机实例,而且保存在Runtime类的静态成员变量instance_中。最后调用Runtime类的成员函数Init对该新创建的ART虚拟机进行初始化。
Runtime类的成员函数Init的实现例如以下所看到的:
bool Runtime::Init(const Options& raw_options, bool ignore_unrecognized) {
...... UniquePtr<ParsedOptions> options(ParsedOptions::Create(raw_options, ignore_unrecognized));
...... heap_ = new gc::Heap(options->heap_initial_size_,
options->heap_growth_limit_,
options->heap_min_free_,
options->heap_max_free_,
options->heap_target_utilization_,
options->heap_maximum_size_,
options->image_,
options->is_concurrent_gc_enabled_,
options->parallel_gc_threads_,
options->conc_gc_threads_,
options->low_memory_mode_,
options->long_pause_log_threshold_,
options->long_gc_log_threshold_,
options->ignore_max_footprint_);
...... java_vm_ = new JavaVMExt(this, options.get());
...... Thread* self = Thread::Attach("main", false, NULL, false);
...... if (GetHeap()->GetContinuousSpaces()[0]->IsImageSpace()) {
class_linker_ = ClassLinker::CreateFromImage(intern_table_);
} else {
......
class_linker_ = ClassLinker::CreateFromCompiler(*options->boot_class_path_, intern_table_);
}
...... return true;
}
这个函数定义在文件art/runtime/runtime.cc中。
Runtime类的成员函数Init首先调用ParsedOptions类的静态成员函数Create对ART虚拟机的启动參数raw_options进行解析。解析后得到的參数保存在一个ParsedOptions对象中。接下来就依据这些參数一个ART虚拟机堆。ART虚拟机堆使用一个Heap对象来描写叙述。
创建好ART虚拟机堆后。Runtime类的成员函数Init接着又创建了一个JavaVMExt实例。
这个JavaVMExt实例终于是要返回给调用者的,使得调用者能够通过该JavaVMExt实例来和ART虚拟机交互。再接下来,Runtime类的成员函数Init通过Thread类的成员函数Attach将当前线程作为ART虚拟机的主线程。使得当前线程能够调用ART虚拟机提供的JNI接口。
Runtime类的成员函数GetHeap返回的便是当前ART虚拟机的堆,也就是前面创建的ART虚拟机堆。通过调用Heap类的成员函数GetContinuousSpaces能够获得堆里面的连续空间列表。假设这个列表的第一个连续空间是一个Image空间,那么就调用ClassLinker类的静态成员函数CreateFromImage来创建一个ClassLinker对象。
否则的话,上述ClassLinker对象就要通过ClassLinker类的另外一个静态成员函数CreateFromCompiler来创建。创建出来的ClassLinker对象是后面ART虚拟机载入载入Java类时要用到的。
后面我们分析ART虚拟机的垃圾收集机制时会看到。ART虚拟机的堆包括有三个连续空间和一个不连续空间。三个连续空间分别用来分配不同的对象。当第一个连续空间不是Image空间时。就表明当前进程不是Zygote进程,而是安装应用程序时启动的一个dex2oat进程。安装应用程序时启动的dex2oat进程也会在内部创建一个ART虚拟机。只是这个ART虚拟机是用来将DEX字节码编译成本地机器指令的,而Zygote进程创建的ART虚拟机是用来执行应用程序的。
接下来我们主要分析ParsedOptions类的静态成员函数Create和ART虚拟机堆Heap的构造函数。以便能够了解ART虚拟机的启动參数解析过程和ART虚拟机的堆创建过程。
ParsedOptions类的静态成员函数Create的实现例如以下所看到的:
Runtime::ParsedOptions* Runtime::ParsedOptions::Create(const Options& options, bool ignore_unrecognized) {
UniquePtr<ParsedOptions> parsed(new ParsedOptions());
const char* boot_class_path_string = getenv("BOOTCLASSPATH");
if (boot_class_path_string != NULL) {
parsed->boot_class_path_string_ = boot_class_path_string;
}
...... parsed->is_compiler_ = false;
...... for (size_t i = 0; i < options.size(); ++i) {
const std::string option(options[i].first);
...... if (StartsWith(option, "-Xbootclasspath:")) {
parsed->boot_class_path_string_ = option.substr(strlen("-Xbootclasspath:")).data();
} else if (option == "bootclasspath") {
parsed->boot_class_path_
= reinterpret_cast<const std::vector<const DexFile*>*>(options[i].second);
} else if (StartsWith(option, "-Ximage:")) {
parsed->image_ = option.substr(strlen("-Ximage:")).data();
} else if (......) {
......
} else if (option == "compiler") {
parsed->is_compiler_ = true;
} else {
......
}
}
...... if (!parsed->is_compiler_ && parsed->image_.empty()) {
parsed->image_ += GetAndroidRoot();
parsed->image_ += "/framework/boot.art";
} ...... return parsed.release();
}
这个函数定义在文件art/runtime/runtime.cc中。
ART虚拟机的启动參数比較多,这里我们仅仅关注两个:-Xbootclasspath、-Ximage和compiler。
參数-Xbootclasspath用来指定启动类路径。
假设没有指定启动类路径,那么默认的启动类路径就通过环境变量BOOTCLASSPATH来获得。
參数-Ximage用来指定ART虚拟机所使用的Image文件。这个Image是用来启动ART虚拟机的。
參数compiler用来指定当前要创建的ART虚拟机是用来将DEX字节码编译成本地机器指令的。
假设没有指定Image文件,而且当前创建的ART虚拟机又不是用来编译DEX字节码的,那么就将该Image文件指定为设备上的/system/framework/boot.art文件。我们知道,system分区的文件都是在制作ROM时打包进去的。这样上述代码的逻辑就是说,假设没有指定Image文件,那么将system分区预先准备好的framework/boot.art文件作为Image文件来启动ART虚拟机。只是。/system/framework/boot.art文件可能是不存在的。在这样的情况下,就须要生成一个新的Image文件。这个Image文件就是一个包括了多个DEX文件的OAT文件。
接下来通过分析ART虚拟机堆的创建过程就会清楚地看到这一点。
Heap类的构造函数的实现例如以下所看到的:
Heap::Heap(size_t initial_size, size_t growth_limit, size_t min_free, size_t max_free,
double target_utilization, size_t capacity, const std::string& original_image_file_name,
bool concurrent_gc, size_t parallel_gc_threads, size_t conc_gc_threads,
bool low_memory_mode, size_t long_pause_log_threshold, size_t long_gc_log_threshold,
bool ignore_max_footprint)
: ...... {
...... std::string image_file_name(original_image_file_name);
if (!image_file_name.empty()) {
space::ImageSpace* image_space = space::ImageSpace::Create(image_file_name);
......
AddContinuousSpace(image_space);
......
} ......
}
这个函数定义在文件art/runtime/gc/heap.cc中。
ART虚拟机堆的具体创建过程我们在后面分析ART虚拟机的垃圾收集机制时再分析。这里仅仅关注与Image文件相关的逻辑。
參数original_image_file_name描写叙述的就是前面提到的Image文件的路径。假设它的值不等于空的话,那么就以它为參数。调用ImageSpace类的静态成员函数Create创建一个Image空间。而且调用Heap类的成员函数AddContinuousSpace将该Image空间作为本进程的ART虚拟机堆的第一个连续空间。
接下来我们继续分析ImageSpace类的静态成员函数Create。它的实现例如以下所看到的:
ImageSpace* ImageSpace::Create(const std::string& original_image_file_name) {
if (OS::FileExists(original_image_file_name.c_str())) {
// If the /system file exists, it should be up-to-date, don't try to generate
return space::ImageSpace::Init(original_image_file_name, false);
}
// If the /system file didn't exist, we need to use one from the dalvik-cache.
// If the cache file exists, try to open, but if it fails, regenerate.
// If it does not exist, generate.
std::string image_file_name(GetDalvikCacheFilenameOrDie(original_image_file_name));
if (OS::FileExists(image_file_name.c_str())) {
space::ImageSpace* image_space = space::ImageSpace::Init(image_file_name, true);
if (image_space != NULL) {
return image_space;
}
}
CHECK(GenerateImage(image_file_name)) << "Failed to generate image: " << image_file_name;
return space::ImageSpace::Init(image_file_name, true);
}
这个函数定义在文件art/runtime/gc/space/image_space.cc中。
ImageSpace类的静态成员函数Create首先是检查參数original_image_file_name指定的Image文件是否存在。假设存在的话。就以它为參数。调用ImageSpace类的另外一个静态成员函数Init来创建一个Image空间。否则的话,再调用函数GetDalvikCacheFilenameOrDie依据參数original_image_file_name构造另外一个在/data/dalvik-cache文件夹下的文件路径。然后再检查这个文件是否存在。假设存在的话,就相同是以它为參数,调用ImageSpace类的静态成员函数Init来创建一个Image空间。否则的话。就要调用ImageSpace类的另外一个静态成员函数GenerateImage来生成一个新的Image文件,接着再调用ImageSpace类的静态成员函数Init来创建一个Image空间了。
我们假设參数original_image_file_name的值等于“/system/framework/boot.art”,那么ImageSpace类的静态成员函数Create的执行逻辑实际上就是:
1. 检查文件/system/framework/boot.art是否存在。假设存在,那么就以它为參数,创建一个Image空间。
否则的话,执行下一步。
2. 检查文件/data/dalvik-cache/system@framework@boot.art@classes.dex是否存在。
假设存在,那么就以它为參数,创建一个Image空间。否则的话。执行下一步。
3. 调用ImageSpace类的静态成员函数GenerateImage在/data/dalvik-cache文件夹下生成一个system@framework@boot.art@classes.dex。然后再以该文件为參数,创建一个Image空间。
接下来我们再来看看ImageSpace类的静态成员函数GenerateImage的实现,例如以下所看到的:
static bool GenerateImage(const std::string& image_file_name) {
const std::string boot_class_path_string(Runtime::Current()->GetBootClassPathString());
std::vector<std::string> boot_class_path;
Split(boot_class_path_string, ':', boot_class_path);
...... std::vector<std::string> arg_vector; std::string dex2oat(GetAndroidRoot());
dex2oat += (kIsDebugBuild ? "/bin/dex2oatd" : "/bin/dex2oat");
arg_vector.push_back(dex2oat); std::string image_option_string("--image=");
image_option_string += image_file_name;
arg_vector.push_back(image_option_string);
...... for (size_t i = 0; i < boot_class_path.size(); i++) {
arg_vector.push_back(std::string("--dex-file=") + boot_class_path[i]);
} std::string oat_file_option_string("--oat-file=");
oat_file_option_string += image_file_name;
oat_file_option_string.erase(oat_file_option_string.size() - 3);
oat_file_option_string += "oat";
arg_vector.push_back(oat_file_option_string);
...... if (kIsTargetBuild) {
arg_vector.push_back("--image-classes-zip=/system/framework/framework.jar");
arg_vector.push_back("--image-classes=preloaded-classes");
}
...... // Convert the args to char pointers.
std::vector<char*> char_args;
for (std::vector<std::string>::iterator it = arg_vector.begin(); it != arg_vector.end();
++it) {
char_args.push_back(const_cast<char*>(it->c_str()));
}
char_args.push_back(NULL); // fork and exec dex2oat
pid_t pid = fork();
if (pid == 0) {
...... execv(dex2oat.c_str(), &char_args[0]); ......
return false;
} else {
...... // wait for dex2oat to finish
int status;
pid_t got_pid = TEMP_FAILURE_RETRY(waitpid(pid, &status, 0));
.......
}
return true;
}
这个函数定义在文件art/runtime/gc/space/image_space.cc中。
ImageSpace类的静态成员函数GenerateImage实际上就调用dex2oat工具在/data/dalvik-cache文件夹下生成两个文件:system@framework@boot.art@classes.dex和system@framework@boot.art@classes.oat。
system@framework@boot.art@classes.dex是一个Image文件,通过--image选项传递给dex2oat工具,里面包括了一些须要在Zygote进程启动时预载入的类。这些须要预载入的类由/system/framework/framework.jar文件中面的preloaded-classes文件指定。
system@framework@boot.art@classes.oat是一个OAT文件,通过--oat-file选项传递给dex2oat工具,它是由系统启动路径中指定的jar文件生成的。每个jar文件都通过一个--dex-file选项传递给dex2oat工具。
这样dex2oat工具就能够将它们所包括的classes.dex文件中面的DEX字节码翻译成本地机器指令。
这样,我们就得到了一个包括有多个DEX文件的OAT文件system@framework@boot.art@classes.oat。
通过上面的分析,我们就清楚地看到了ART执行时所须要的OAT文件是怎样产生的了。当中,由系统启动类路径指定的DEX文件生成的OAT文件称为类型为BOOT的OAT文件。即boot.art文件。有了这个背景知识之后,接下来我们就继续分析ART执行时是怎样载入OAT文件的。
ART执行时提供了一个OatFile类,通过调用它的静态成员函数Open能够在本进程中载入OAT文件,它的实现例如以下所看到的:
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中。
參数filename和location实际上是一样的。指向要载入的OAT文件。
參数requested_base是一个可选參数。用来描写叙述要载入的OAT文件中面的oatdata段要载入在的位置。參数executable表示要载入的OAT是不是应用程序的主执行文件。一般来说。一个应用程序仅仅有一个classes.dex文件。 这个classes.dex文件经过编译后,就得到一个OAT主执行文件。
只是,应用程序也能够在执行时动态载入DEX文件。这些动态载入的DEX文件在载入的时候相同会被翻译成OAT再执行,它们相应打包在应用程序的classes.dex文件来说,就不属于主执行文件了。
OatFile类的静态成员函数Open的实现尽管仅仅有寥寥几行代码,可是要理解它还得先理解宏ART_USE_PORTABLE_COMPILER的的作用。在前面Android执行时ART简要介绍和学习计划一文中提到,ART执行时利用LLVM编译框架来将DEX字节码翻译成本地机器指令,当中要通过一个称为Backend的模块来生成本地机器指令。这些生成的机器指令就保存在ELF文件格式的OAT文件的oatexec段中。
ART执行时会为每个类方法都生成一系列的本地机器指令。
这些本地机器指令不是孤立存在的。因为它们可能须要其他的函数来完毕自己的功能。比如。它们可能须要调用ART执行时的堆管理系统提供的接口来为对象分配内存空间。这样就会涉及到一个模块依赖性问题,就好像我们在编敲代码时。须要依赖C库提供的接口一样。这要求Backend为类方法生成本地机器指令时,要处理调用其他模块提供的函数的问题。
ART执行时支持两种类型的Backend:Portable和Quick。
Portable类型的Backend通过集成在LLVM编译框架里面的一个称为MCLinker的链接器来生成本地机器指令。关于MCLinker的很多其他知识。能够參考https://code.google.com/p/mclinker。简单来说。假设我们有一个模块A,它依赖于模块B、C和D,那么在为模块A生成本地机器指令时,指出它依赖于模块B、C和D即可了。在生成的OAT文件中会记录好这些依赖关系。这是ELF文件格式本来就支持的特性。
这些OAT文件要通过系统的动态链接器提供的dlopen函数来载入。函数dlopen在载入OAT文件的时候,会通过重定位技术来处理好它与其他模块的依赖关系,使得它能够调用其他模块提供的接口。这个实际上就通用的编译器、静态连接器以及动态链接器合作在一起干的事情,MCLinker扮演的就是静态链接器的角色。既然是通用的技术,因为就称能产生这样的OAT文件的Backend为Portable类型的。
还有一方面,Quick类型的Backend生成的本地机器指令用第二种方式来处理依赖模块之间的依赖关系。
简单来说,就是ART执行时会在每个线程的TLS(线程本地区域)提供一个函数表。有了这个函数表之后。Quick类型的Backend生成的本地机器指令就能够通过它来调用其他模块的函数。也就是说,Quick类型的Backend生成的本地机器指令要依赖于ART执行时提供的函数表。这使得Quick类型的Backend生成的OAT文件在载入时不须要再处理模式之间的依赖关系。再通俗一点说的就是Quick类型的Backend生成的OAT文件在载入时不须要重定位,因此就不须要通过系统的动态链接器提供的dlopen函数来载入。因为省去重定位这个操作。Quick类型的Backend生成的OAT文件在载入时就会更快,这也是称为Quick的缘由。
关于ART执行时类型为Portable和Quick两种类型的Backend,我们就临时解讲到这里。后面分析ART执行时执行类方法的时候,我们再具体分析。
如今我们须要知道的就是,假设在编译ART执行时时。定义了宏ART_USE_PORTABLE_COMPILER。那么就表示要使用Portable类型的Backend来生成OAT文件,否则就使用Quick类型的Backend来生成OAT文件。默认情况下。使用的是Quick类型的Backend。
接下就能够非常好地理解OatFile类的静态成员函数Open的实现了:
1. 假设编译时指定了ART_USE_PORTABLE_COMPILER宏,而且參数executable为true,那么就通过OatFile类的静态成员函数OpenDlopen来载入指定的OAT文件。
OatFile类的静态成员函数OpenDlopen直接通过动态链接器提供的dlopen函数来载入OAT文件。
2. 其余情况下,通过OatFile类的静态成员函数OpenElfFile来手动载入指定的OAT文件。这样的方式是依照ELF文件格式来解析要载入的OAT文件的,而且依据解析获得的信息将OAT里面相应的段载入到内存中来。
接下来我们就分别看看OatFile类的静态成员函数OpenDlopen和OpenElfFile的实现,以便能够对OAT文件有更清楚的认识。
OatFile类的静态成员函数OpenDlopen的实现例如以下所看到的:
OatFile* OatFile::OpenDlopen(const std::string& elf_filename,
const std::string& location,
byte* requested_base) {
UniquePtr<OatFile> oat_file(new OatFile(location));
bool success = oat_file->Dlopen(elf_filename, requested_base);
if (!success) {
return NULL;
}
return oat_file.release();
}
这个函数定义在文件art/runtime/oat_file.cc中。
OatFile类的静态成员函数OpenDlopen首先是创建一个OatFile对象,接着再调用该OatFile对象的成员函数Dlopen载入參数elf_filename指定的OAT文件。
OatFile类的成员函数Dlopen的实现例如以下所看到的:
bool OatFile::Dlopen(const std::string& elf_filename, byte* requested_base) {
char* absolute_path = realpath(elf_filename.c_str(), NULL);
...... dlopen_handle_ = dlopen(absolute_path, RTLD_NOW);
...... begin_ = reinterpret_cast<byte*>(dlsym(dlopen_handle_, "oatdata"));
...... if (requested_base != NULL && begin_ != requested_base) {
......
return false;
} end_ = reinterpret_cast<byte*>(dlsym(dlopen_handle_, "oatlastword"));
...... // Readjust to be non-inclusive upper bound.
end_ += sizeof(uint32_t);
return Setup();
}
这个函数定义在文件art/runtime/oat_file.cc中。
OatFile类的成员函数Dlopen首先是通过动态链接器提供的dlopen函数将參数elf_filename指定的OAT文件载入到内存中来。接着相同是通过动态链接器提供的dlsym函数从载入进来的OAT文件获得两个导出符号oatdata和oatlastword的地址。分别保存在当前正在处理的OatFile对象的成员变量begin_和end_中。依据图1所看到的,符号oatdata的地址即为OAT文件中面的oatdata段载入到内存中的開始地址,而符号oatlastword的地址即为OAT文件中面的oatexec载入到内存中的结束地址。
符号oatlastword本身也是属于oatexec段的。它自己占用了一个地址,也就是sizeof(uint32_t)个字节。于是将前面得到的end_值加上sizeof(uint32_t),得到的才是oatexec段的结束地址。
实际上,上面得到的begin_值指向的是载入内存中的oatdata段的头部。即OAT头。
这个OAT头描写叙述了OAT文件所包括的DEX文件的信息。以及定义在这些DEX文件中面的类方法所相应的本地机器指令在内存的位置。
另外,上面得到的end_是用来在解析OAT头时验证数据的正确性的。此外。假设參数requested_base的值不等于0,那么就要求oatdata段必须要载入到requested_base指定的位置去,也就是上面得到的begin_值与requested_base值相等,否则的话就会出错返回。
最后。OatFile类的成员函数Dlopen通过调用另外一个成员函数Setup来解析已经载入内存中的oatdata段,以获得ART执行时所须要的很多其他信息。我们分析完毕OatFile类的静态成员函数OpenElfFile之后,再来看OatFile类的成员函数Setup的实现。
OatFile类的静态成员函数OpenElfFile的实现例如以下所看到的:
OatFile* OatFile::OpenElfFile(File* file,
const std::string& location,
byte* requested_base,
bool writable,
bool executable) {
UniquePtr<OatFile> oat_file(new OatFile(location));
bool success = oat_file->ElfFileOpen(file, requested_base, writable, executable);
if (!success) {
return NULL;
}
return oat_file.release();
}
这个函数定义在文件art/runtime/oat_file.cc中。
OatFile类的静态成员函数OpenElfFile创建了一个OatFile对象后。就调用它的成员函数ElfFileOpen来执行载入OAT文件的工作,它的实现例如以下所看到的:
bool OatFile::ElfFileOpen(File* file, byte* requested_base, bool writable, bool executable) {
elf_file_.reset(ElfFile::Open(file, writable, true));
......
bool loaded = elf_file_->Load(executable);
......
begin_ = elf_file_->FindDynamicSymbolAddress("oatdata");
......
if (requested_base != NULL && begin_ != requested_base) {
......
return false;
}
end_ = elf_file_->FindDynamicSymbolAddress("oatlastword");
......
// Readjust to be non-inclusive upper bound.
end_ += sizeof(uint32_t);
return Setup();
}
这个函数定义在文件art/runtime/oat_file.cc中。
OatFile类的静态成员函数OpenElfFile的实现与前面分析的成员函数Dlopen是非常相似的,唯一不同的是前者通过ElfFile类来手动载入參数file指定的OAT文件,实际上就是依照ELF文件格式来解析參数file指定的OAT文件,而且将文件中面的oatdata段和oatexec段载入到内存中来。我们能够将ElfFile类看作是ART执行时自己实现的OAT文件动态链接器。一旦參数file指定的OAT文件指定的文件载入完毕之后,我们相同是通过两个导出符号oatdata和oatlastword来获得oatdata段和oatexec段的起止位置。相同,假设參数requested_base的值不等于0,那么就要求oatdata段必须要载入到requested_base指定的位置去。
将參数file指定的OAT文件载入到内存之后,OatFile类的静态成员函数OpenElfFile最后也是调用OatFile类的成员函数Setup来解析当中的oatdata段。OatFile类的成员函数Setup定义在文件art/runtime/oat_file.cc中,我们分三部分来阅读。以便能够更好地理解OAT文件的格式。
OatFile类的成员函数Setup的第一部分实现例如以下所看到的:
bool OatFile::Setup() {
if (!GetOatHeader().IsValid()) {
LOG(WARNING) << "Invalid oat magic for " << GetLocation();
return false;
}
const byte* oat = Begin();
oat += sizeof(OatHeader);
if (oat > End()) {
LOG(ERROR) << "In oat file " << GetLocation() << " found truncated OatHeader";
return false;
}
我们先来看OatFile类的三个成员函数GetOatHeader、Begin和End的实现。例如以下所看到的:
const OatHeader& OatFile::GetOatHeader() const {
return *reinterpret_cast<const OatHeader*>(Begin());
} const byte* OatFile::Begin() const {
CHECK(begin_ != NULL);
return begin_;
} const byte* OatFile::End() const {
CHECK(end_ != NULL);
return end_;
}
这三个函数主要是涉及到了OatFile类的两个成员变量begin_和end_,它们各自是OAT文件中面的oatdata段開始地址和oatexec段的结束地址。
通过OatFile类的成员函数GetOatHeader能够清楚地看到。OAT文件中面的oatdata段的開始储存着一个OAT头,这个OAT头通过类OatHeader描写叙述,定义在文件art/runtime/oat.h中,例如以下所看到的:
class PACKED(4) OatHeader {
public:
......
private:
uint8_t magic_[4];
uint8_t version_[4];
uint32_t adler32_checksum_; InstructionSet instruction_set_;
uint32_t dex_file_count_;
uint32_t executable_offset_;
uint32_t interpreter_to_interpreter_bridge_offset_;
uint32_t interpreter_to_compiled_code_bridge_offset_;
uint32_t jni_dlsym_lookup_offset_;
uint32_t portable_resolution_trampoline_offset_;
uint32_t portable_to_interpreter_bridge_offset_;
uint32_t quick_resolution_trampoline_offset_;
uint32_t quick_to_interpreter_bridge_offset_; uint32_t image_file_location_oat_checksum_;
uint32_t image_file_location_oat_data_begin_;
uint32_t image_file_location_size_;
uint8_t image_file_location_data_[0]; // note variable width data at end ......
};
类OatHeader的各个成员变量的含义例如以下所看到的:
magic: 标志OAT文件的一个魔数,等于‘oat\n’。
version: OAT文件版本。眼下的值等于‘007、0’。
adler32_checksum_: OAT头部检验和。
instruction_set_: 本地机指令集,有四种取值。分别为 kArm(1)、kThumb2(2)、kX86(3)和kMips(4)。
dex_file_count_: OAT文件包括的DEX文件个数。
executable_offset_: oatexec段開始位置与oatdata段開始位置的偏移值。
interpreter_to_interpreter_bridge_offset_和interpreter_to_compiled_code_bridge_offset_: ART执行时在启动的时候。能够通过-Xint选项指定全部类的方法都是解释执行的,这与传统的虚拟机使用解释器来执行类方法差点儿相同。同一时候,有些类方法可能没有被翻译成本地机器指令,这时候也要求对它们进行解释执行。这意味着解释执行的类方法在执行的过程中,可能会调用到另外一个也是解释执行的类方法,也可能调用到另外一个按本地机器指令执行的类方法中。
OAT文件在内部提供有两段trampoline代码。分别用来从解释器调用另外一个也是通过解释器来执行的类方法和从解释器调用另外一个依照本地机器执行的类方法。这两段trampoline代码的偏移位置就保存在成员变量 interpreter_to_interpreter_bridge_offset_和interpreter_to_compiled_code_bridge_offset_。
jni_dlsym_lookup_offset_: 类方法在执行的过程中,假设要调用另外一个方法是一个JNI函数,那么就要通过存在放置jni_dlsym_lookup_offset_的一段trampoline代码来调用。
portable_resolution_trampoline_offset_和quick_resolution_trampoline_offset_: 用来在执行时解析还未链接的类方法的两段trampoline代码。当中。portable_resolution_trampoline_offset_指向的trampoline代码用于Portable类型的Backend生成的本地机器指令。而quick_resolution_trampoline_offset_用于Quick类型的Backend生成的本地机器指令。
portable_to_interpreter_bridge_offset_和quick_to_interpreter_bridge_offset_: 与interpreter_to_interpreter_bridge_offset_和interpreter_to_compiled_code_bridge_offset_的作用刚好相反。用来在依照本地机器指令执行的类方法中调用解释执行的类方法的两段trampoline代码。当中,portable_to_interpreter_bridge_offset_用于Portable类型的Backend生成的本地机器指令,而quick_to_interpreter_bridge_offset_用于Quick类型的Backend生成的本地机器指令。
因为每个应用程序都会依赖于boot.art文件,因此为了节省由打包在应用程序里面的classes.dex生成的OAT文件的体积,上述interpreter_to_interpreter_bridge_offset_、interpreter_to_compiled_code_bridge_offset_、jni_dlsym_lookup_offset_、portable_resolution_trampoline_offset_、portable_to_interpreter_bridge_offset_、quick_resolution_trampoline_offset_和quick_to_interpreter_bridge_offset_七个成员变量指向的trampoline代码段仅仅存在于boot.art文件中。换句话说,在由打包在应用程序里面的classes.dex生成的OAT文件的oatdata段头部中。上述七个成员变量的值均等于0。
image_file_location_data_: 用来创建Image空间的文件的路径的在内存中的地址。
image_file_location_size_: 用来创建Image空间的文件的路径的大小。
image_file_location_oat_data_begin_: 用来创建Image空间的OAT文件的oatdata段在内存的位置。
image_file_location_oat_checksum_: 用来创建Image空间的OAT文件的检验和。
上述四个成员变量记录了一个OAT文件所依赖的用来创建Image空间文件以及创建这个Image空间文件所使用的OAT文件的相关信息。
通过OatFile类的成员函数Setup的第一部分代码的分析,我们就知道了,OAT文件的oatdata段在最開始保存着一个OAT头。如图2所看到的:
图2 OAT头部
我们接着再看OatFile类的成员函数Setup的第二部分代码:
oat += GetOatHeader().GetImageFileLocationSize();
if (oat > End()) {
LOG(ERROR) << "In oat file " << GetLocation() << " found truncated image file location: "
<< reinterpret_cast<const void*>(Begin())
<< "+" << sizeof(OatHeader)
<< "+" << GetOatHeader().GetImageFileLocationSize()
<< "<=" << reinterpret_cast<const void*>(End());
return false;
}
调用OatFile类的成员函数GetOatHeader获得的是正在打开的OAT文件的头部OatHeader。通过调用它的成员函数GetImageFileLocationSize获得的是正在打开的OAT依赖的Image空间文件的路径大小。
变量oat最開始的时候指向oatdata段的開始位置。读出OAT头之后。变量oat就跳过了OAT头。
因为正在打开的OAT文件引用的Image空间文件路径保存在紧接着OAT头的地方。
因此,将Image空间文件的路径大小添加到变量oat去后。就相当于是跳过了保存Image空间文件路径的位置。
通过OatFile类的成员函数Setup的第二部分代码的分析。我们就知道了,紧接着在OAT头后面的是Image空间文件路径,如图3所看到的:
图3 OAT头和Image空间文件路径
我们接着再看OatFile类的成员函数Setup的第三部分代码:
for (size_t i = 0; i < GetOatHeader().GetDexFileCount(); i++) {
size_t dex_file_location_size = *reinterpret_cast<const uint32_t*>(oat);
...... oat += sizeof(dex_file_location_size);
...... const char* dex_file_location_data = reinterpret_cast<const char*>(oat);
oat += dex_file_location_size;
...... std::string dex_file_location(dex_file_location_data, dex_file_location_size); uint32_t dex_file_checksum = *reinterpret_cast<const uint32_t*>(oat);
oat += sizeof(dex_file_checksum);
...... uint32_t dex_file_offset = *reinterpret_cast<const uint32_t*>(oat);
...... oat += sizeof(dex_file_offset);
...... const uint8_t* dex_file_pointer = Begin() + dex_file_offset;
if (!DexFile::IsMagicValid(dex_file_pointer)) {
......
return false;
}
if (!DexFile::IsVersionValid(dex_file_pointer)) {
......
return false;
} const DexFile::Header* header = reinterpret_cast<const DexFile::Header*>(dex_file_pointer);
const uint32_t* methods_offsets_pointer = reinterpret_cast<const uint32_t*>(oat); oat += (sizeof(*methods_offsets_pointer) * header->class_defs_size_);
...... oat_dex_files_.Put(dex_file_location, new OatDexFile(this,
dex_file_location,
dex_file_checksum,
dex_file_pointer,
methods_offsets_pointer));
}
return true;
}
这部分代码用来获得包括在oatdata段的DEX文件描写叙述信息。每个DEX文件记录在oatdata段的描写叙述信息包括:
1. DEX文件路径大小,保存在变量dex_file_location_size中;
2. DEX文件路径。保存在变量dex_file_location_data中;
3. DEX文件检验和,保存在变量dex_file_checksum中。
4. DEX文件内容在oatdata段的偏移,保存在变量dex_file_offset中;
5. DEX文件包括的类的本地机器指令信息偏移数组。保存在变量methods_offsets_pointer中;
在上述五个信息中。最重要的就是第4个和第5个信息了。
通过第4个信息,我们能够在oatdata段中找到相应的DEX文件的内容。DEX文件最開始部分是一个DEX文件头,上述代码通过检查DEX文件头的魔数和版本来确保变量dex_file_offset指向的位置确实是一个DEX文件。
通过第5个信息我们能够找到DEX文件中面的每个类方法相应的本地机器指令。
这个数组的大小等于header->class_defs_size_,即DEX文件中面的每个类在数组中都相应有一个偏移值。
这里的header指向的是DEX文件头,它的class_defs_size_描写叙述了DEX文件包括的类的个数。
在DEX文件中。每个类都是有一个从0開始的编号,该编号就是用来索引到上述数组的,从而获得相应的类全部方法的本地机器指令信息。
最后,上述得到的每个DEX文件的信息都被封装在一个OatDexFile对象中。以便以后能够直接訪问。假设我们使用OatDexFile来描写叙述每个DEX文件的描写叙述信息。那么就能够通过图4看到这些描写叙述信息在oatdata段的位置:
图4 OAT头、Image空间文件路径、DEX文件描写叙述信息
为了进一步理解包括在oatdata段的DEX文件描写叙述信息,我们继续看OatDexFile类的构造函数的实现,例如以下所看到的:
OatFile::OatDexFile::OatDexFile(const OatFile* oat_file,
const std::string& dex_file_location,
uint32_t dex_file_location_checksum,
const byte* dex_file_pointer,
const uint32_t* oat_class_offsets_pointer)
: oat_file_(oat_file),
dex_file_location_(dex_file_location),
dex_file_location_checksum_(dex_file_location_checksum),
dex_file_pointer_(dex_file_pointer),
oat_class_offsets_pointer_(oat_class_offsets_pointer) {}
这个函数定义在文件art/runtime/oat_file.cc中。
OatDexFile类的构造函数的实现非常easy,它将我们在上面得到的DEX文件描写叙述息保存在相应的成员变量中。通过这些信息,我们就能够获得包括在该DEX文件中面的类的全部方法的本地机器指令信息。
比如,通过调用OatDexFile类的成员函数GetOatClass能够获得指定类的全部方法的本地机器指令信息:
const OatFile::OatClass* OatFile::OatDexFile::GetOatClass(uint16_t class_def_index) const {
uint32_t oat_class_offset = oat_class_offsets_pointer_[class_def_index]; const byte* oat_class_pointer = oat_file_->Begin() + oat_class_offset;
CHECK_LT(oat_class_pointer, oat_file_->End()) << oat_file_->GetLocation();
mirror::Class::Status status = *reinterpret_cast<const mirror::Class::Status*>(oat_class_pointer); const byte* methods_pointer = oat_class_pointer + sizeof(status);
CHECK_LT(methods_pointer, oat_file_->End()) << oat_file_->GetLocation(); return new OatClass(oat_file_,
status,
reinterpret_cast<const OatMethodOffsets*>(methods_pointer));
}
这个函数定义在文件art/runtime/oat_file.cc中。
參数class_def_index表示要查找的目标类的编号。这个编号用作数组oat_class_offsets_pointer_(即前面描写叙述的methods_offsets_pointer数组)的索引。就能够得到一个偏移位置oat_class_offset。这个偏移位置是相对于OAT文件的oatdata段的,因此将该偏移值加上OAT文件的oatdata段的開始位置后,就能够得到目标类的全部方法的本地机器指令信息。这些信息的布局如图5所看到的:
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvTHVvc2hlbmd5YW5n/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />
图5 DEX文件中面的类描写叙述信息
在OAT文件中。每个DEX文件包括的每个类的描写叙述信息都通过一个OatClass对象来描写叙述。为了方便描写叙述,我们称之为OAT类。我们通过OatClass类的构造函数来理解它的作用,例如以下所看到的:
OatFile::OatClass::OatClass(const OatFile* oat_file,
mirror::Class::Status status,
const OatMethodOffsets* methods_pointer)
: oat_file_(oat_file), status_(status), methods_pointer_(methods_pointer) {}
这个函数定义在文件art/runtime/oat_file.cc中。
參数oat_file描写叙述的是宿主OAT文件,參数status描写叙述的是OAT类状态,參数methods_pointer是一个数组,描写叙述的是OAT类的各个方法的信息,它们被分别保存在OatClass类的相应成员变量中。通过这些信息,我们就能够获得包括在该DEX文件中面的类的全部方法的本地机器指令信息。
比如,通过调用OatClass类的成员函数GetOatMethod能够获得指定类方法的本地机器指令信息:
const OatFile::OatMethod OatFile::OatClass::GetOatMethod(uint32_t method_index) const {
const OatMethodOffsets& oat_method_offsets = methods_pointer_[method_index];
return OatMethod(
oat_file_->Begin(),
oat_method_offsets.code_offset_,
oat_method_offsets.frame_size_in_bytes_,
oat_method_offsets.core_spill_mask_,
oat_method_offsets.fp_spill_mask_,
oat_method_offsets.mapping_table_offset_,
oat_method_offsets.vmap_table_offset_,
oat_method_offsets.gc_map_offset_);
}
这个函数定义在文件art/runtime/oat_file.cc中。
參数method_index描写叙述的目标方法在类中的编号。用这个编号作为索引,就能够在OatClass类的成员变量methods_pointer_指向的一个数组中找到目标方法的本地机器指令信息。这些本地机器指令信息封装在一个OatMethod对象,它们在OAT文件的布局如图6下所看到的:
图6 DEX文件中面的类(OatClass)描写叙述信息
为了进一步理解OatMethod的作用。我们继续看它的构造函数的实现,例如以下所看到的:
OatFile::OatMethod::OatMethod(const byte* base,
const uint32_t code_offset,
const size_t frame_size_in_bytes,
const uint32_t core_spill_mask,
const uint32_t fp_spill_mask,
const uint32_t mapping_table_offset,
const uint32_t vmap_table_offset,
const uint32_t gc_map_offset)
: begin_(base),
code_offset_(code_offset),
frame_size_in_bytes_(frame_size_in_bytes),
core_spill_mask_(core_spill_mask),
fp_spill_mask_(fp_spill_mask),
mapping_table_offset_(mapping_table_offset),
vmap_table_offset_(vmap_table_offset),
native_gc_map_offset_(gc_map_offset) {
......
}
这个函数定义在文件art/runtime/oat_file.cc中。
OatMethod类包括了非常多相应类方法的本地机器指令执行时要用到的信息。当中,最重要的就是參数base和code_offset描写叙述的信息。
參数base描写叙述的是OAT文件的OAT头在内存的位置,而參数code_offset描写叙述的是类方法的本地机器指令相对OAT头的偏移位置。将这两者相加,就能够得到一个类方法的本地机器指令在内存的位置。
我们能够通过调用OatMethod类的成员函数GetCode来获得这个结果。
OatMethod类的成员函数GetCode的实现例如以下所看到的:
const void* OatFile::OatMethod::GetCode() const {
return GetOatPointer<const void*>(code_offset_);
}
这个函数定义在文件art/runtime/oat_file.cc中。
OatMethod类的成员函数调用另外一个成员函数GetOatPointer来获得一个类方法的本地机器指令在内存的位置。
OatMethod类的成员函数GetOatPointer的实现例如以下所看到的:
class OatFile {
...... class OatMethod {
...... private:
template<class T>
T GetOatPointer(uint32_t offset) const {
if (offset == 0) {
return NULL;
}
return reinterpret_cast<T>(begin_ + offset);
} ......
}; ......
};
这个函数定义在文件art/runtime/oat_file.h中。
通过上面对OAT文件载入过程的分析。我们就能够清楚地看到OAT文件的格式,以及怎样在OAT文件中找到一个类方法的本地机器指令。
我们通过图7来总结在OAT文件中找到一个类方法的本地机器指令的过程:
图7 在OAT文件中查找类方法的本地机器指令的过程
我们从左往右来看图7。
首先是依据类签名信息从包括在OAT文件中面的DEX文件中查找目标Class的编号,然后再依据这个编号找到在OAT文件中找到相应的OatClass。接下来再依据方法签名从包括在OAT文件中面的DEX文件中查找目标方法的编号,然后再依据这个编号在前面找到的OatClass中找到相应的OatMethod。有了这个OatMethod之后,我们就依据它的成员变量begin_和code_offset_找到目标类方法的本地机器指令了。当中,从DEX文件中依据签名找到类和方法的编号要求对DEX文件进行解析。这就须要利用Dalvik虚拟机的知识了。
至此,我们就通过OAT文件的载入过程分析完毕OAT文件的格式了。为了加深对OAT文件格式的理解,有接下来的一篇文章中,我们再具体分析上面描写叙述的类方法的本地机器指令的查找过程。敬请关注!
很多其他信息也能够关注老罗的新浪微博:http://weibo.com/shengyangluo。
Android执行时ART载入OAT文件的过程分析的更多相关文章
- Android执行时ART载入类和方法的过程分析
在前一篇文章中.我们通过分析OAT文件的载入过程,认识了OAT文件的格式,当中包括了原始的DEX文件. 既然ART运行时运行的都是翻译DEX字节码后得到的本地机器指令了.为什么还须要在OAT文件里包括 ...
- JS Window对象 计时器setInterval() 在执行时,从载入页面后每隔指定的时间执行代码。
计时器setInterval() 在执行时,从载入页面后每隔指定的时间执行代码. 语法: setInterval(代码,交互时间); 参数说明: 1. 代码:要调用的函数或要执行的代码串. 2. 交互 ...
- 【重要】U3D存放本地游戏存档——不同平台载入XML文件的方法——IOS MAC Android
在PC上和IOS上读取XML文件的方式略有差别,经测试,IOS上不支持如下方法载入XML文件: XmlDocument xmlDoc = new XmlDocument(); xmlDoc.Load( ...
- (转)U3D不同平台载入XML文件的方法——IOS MAC Android
自:http://www.cnblogs.com/sifenkesi/archive/2012/03/12/2391330.html 在PC上和IOS上读取XML文件的方式略有差别,经测试,IOS上不 ...
- Android oat文件提取转换
说明: 1.手机厂商可以修改Android源码并进行编译后再生成oat格式文件在手机上存储,比如boot-okhttp.oat,boot-framework.oat. 2.自带的apk可以调用这些模块 ...
- Android odex,oat文件的反编译,回编译
现在,许多Android手机的ROM包在生成过程中都启用优化,把jar文件抽空,生成odex/oat和vdex文件,以在运行时省掉编译时间.如果想对这些jar进行修改,就要修改它们所对应的odex或者 ...
- py文件控制台执行时,报错:引入的模块不存在
1.描述:该模块在IDE中是可以正确执行的.但是从cmd控制台执行时,报错:该模块引入的其他模块不存在. 2.解决:在该模块的#encoding:utf-8 之后另起一行加如下代码: #encodin ...
- Android L 使用ART能提高多少性能?
点击打开链接 刚刚结束的 Google I/O 大会上,Android 下一代操作系统「L」带来不少惊喜.新系统运行更快.更省电. 然而开发者对这个新系统也有颇多疑问,比如新的运行模式 ART 对开发 ...
- Android:What is ART?
背景:Android4.2之前,安卓手机系统的应用程序均在Dalvik Java的虚拟机上执行,这样的执行模式还要依靠一个编译器来实现与应用程序的沟通.应用程序每次执行时,都须要将程序内的代码转变为机 ...
随机推荐
- 3-4 第三天 Generator生成器
Generator是ES6里面的新增规范,ES6其实就是ES2015.ES5.ES6.ES7这些术语大家上网一查就都明白了.JavaScript是一个范程,就是我们说的JS.ES就是ECMA Scri ...
- Human Gene Functions(dp)
http://poj.org/problem?id=1080 #include <stdio.h> #include <stdlib.h> #include <strin ...
- 原生JS---7
原生js学习笔记7——本地存储之cookie操作 什么是cookie • 用来保存页面信息的,如用户名.密码 • cookie的特性:同一个网站中所有的页面共享一套cookie:数量.大小限制:过期时 ...
- [转]RDLC 动态列
本文转自:http://blog.csdn.net/luochengbang/article/details/9964551 很久没有写博客了,关于动态列,国内很少资料有介绍动态列的,所想写点心得给哥 ...
- 【HTTP】长连接和短连接
1. HTTP协议与TCP/IP协议的关系 HTTP的长连接和短连接本质上是TCP长连接和短连接.HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议.IP协议主要解决网络路由和寻址问 ...
- ★Java面向对象(一)——————————基本概念
package boll; /* 用Java语言对现实生活中的事物进行描述. 通过类的形式来体现, 怎么描述呢? 对于事物的描述通常只有两个方面,一个是属性,一个是行为. 只要明确该事物的行为和属性并 ...
- 【技术累积】【点】【git】【10】.gitignore和.gitattributes
.gitignore 告诉git忽略一些文件,git status会显示不到这些文件的状态. 一般放在项目根目录,以对全局控制,当然可以放在module下: 具体规则主要是: 以行为单位定义忽略文件类 ...
- 读书笔记「Python编程:从入门到实践」_5.if语句
5.1 一个简单示例 cars = ['audi', 'bmw', 'subaru', 'toyota'] for car in cars: if car == 'bmw': print(car.up ...
- mqtt-client回调方法简介
mqtt-client回调方法简介 毫无疑问Callback方式是最复杂的一种,但是其也是能够提供更好的服务,因此有必要好好研究,下面就是对使用回调方式的简单介绍: 一.在使用回调方式前,先通过MQT ...
- Java JPA通过hql语句查询数据
import javax.persistence.PersistenceContext; import javax.persistence.Query; public class StudentSer ...