dex2oat介绍

pre-compilation的好处

graph LR
AOT-->Fast
AOT-->cleanMemory

Code pre-compilation: We pre-compile all the hot code. When the apps execute, the most important parts of the code are already optimized and ready to be natively executed. The app no longer needs to wait for the JIT compiler to kick in.

The benefit is that the code is mapped as clean memory (compared to the JIT dirty memory) which improves the overall memory efficiency. The clean memory can be released by the kernel when under memory pressure while the dirty memory cannot, lessening the chances that the kernel will kill the app.

oat文件

JVM执行 java 字节码, Dalvik执行 dalvik 字节码。

ART(Android Runtime),是Android4.4上开始提供的另一个 JVM实现,在4.4时,默认的虚拟机还是 dalvik,ART作为可选项,到Android5.0,开始作为Android默认的虚拟机。

同样的,ART也支持运行 dalvik bytecode(否则没有办法兼容之前的app),另外 ART 提出了一个 AOT(Ahead of time)的方法。

这个 AOT就是相对于 1.2节中提到的 JIT, AOT是说在代码运行之前进行编译。即把dex文件中的 dalvik bytecode编译为处理器可识别执行的汇编指令,我们把编译后生成的代码称为Native code。

而==OAT文件就是包含了dex文件,dex文件编译出的 native Code,以及OAT header,OAT class等组织文件的数据==。

在==使用oat文件的时候,通过这些组织关系,来查找一个类中java函数对应的 native code,从而在执行时去运行 native code==;

实际上app编译出来的==OAT文件是一种特殊的ELF文件,在这个ELF文件的 oatdata 和 oatlastword之间的数据为oat数据。也即 oat文件数据是嵌入在ELF文件中的==。

ART运行的时候,会查询当前app对应的 oat文件进行执行,当找不到oat文件时再解释dex的 bytecode 执行。

简单来讲:ART执行 oat文件,执行其中 java 函数对应 native code; 当函数没有对应的native code或者app没有对应的oat文件时,仍然解释执行dex文件中其对应的 dalvik bytecode。

image-20210531133437431

profile文件

图解

sequenceDiagram
Profile->>installd: createProfile
installd->>installd: create_app_data

Profile->>LoadedApk: startThread
LoadedApk->>LoadedApk: getClassLoader
LoadedApk->>ProfileSaver: recordClassIdAndMethodId
ProfileSaver->>ProfileSaver: start(profileName)

Profile->>BackgroundDexOptService: record

参考应用启动流程

Android7.0之后,ART使用的文件,用来进行 profile-guide编译,即指导 dex2oat 如何编译 dex文件

profile文件:/data/misc/profiles/cur/0/com.***.home/primary.prof

==每个app的profile文件都在 /data/misc/profiles/ 目录下==。profile文件用来记录运行比较频繁的代码,用来进行 profile-guide 编译,使得 dex2oat编译代码更精准。

profile的创建:

App安装的过程中,会调用到 installd的 create_app_data()函数,

如果当前支持profile编译,则会为app创建 profile文件。

Android7.1:

/frameworks/native/cmds/installd/commands.cpp

int create_app_data(const char *uuid, const char *pkgname, userid_t userid, int flags,
        appid_t appid, const char* seinfo, int target_sdk_version) {
      ...
      if (property_get_bool("dalvik.vm.usejitprofiles")) {
            std::string profile_file = create_primary_profile(profile_path);//组织 profile文件所在路径 
            if (fs_prepare_file_strict(profile_file.c_str(), 0600, uid, uid) != 0) {//在这里创建 profile文件,且只对owner Read-Write
                return -1;
            }
       ...
}

profile信息的收集

在App启动的时候,开启profile的收集线程:

->ActivityThread.main()
->...
->ActivityThread.performLaunchActivity()
->ActivityClientRecord.packageInfo.getClassLoader()
->LoadedApk.getClassLoader()
->setupJitProfileSupport()
VMRuntime.registerAppInfo(profileName
Runtime::RegisterAppInfo(profileName)
jit_-> StartProfileSaver(profileName)
ProfileSaver::Start(profilName)//在这里会创建一个thread 用来收集 resolved class与method
 
ProfileSaver::Run() {
FetchAndCacheResolvedClassesAndMethods();
bool profile_saved_to_disk = ProcessProfilingInfo(&new_methods); // 在这个方法中会把达到条件的 methodId 和 classid记录到 profile文件

在这个方法中,会编译当前进程中所有已经Load的Class,如果这些class是apk中的class,则将会被添加到 profile信息中。

对于要记录的 method则需要达到一定的条件(函数的调用次数),函数调用次数有以下几个 threshold:

uint16_t hot_method_threshold_;
uint16_t warm_method_threshold_;
uint16_t osr_method_threshold_;

在解释执行一个函数时,会调用 AddSamples函数,从而会记录函数的调用次数。从而生成profile文件。生成的profile文件格式如下:

profile文件格式:

/**
 * Serialization format:
 *    magic,version,number_of_lines
 *    dex_location1,number_of_methods1,number_of_classes1,dex_location_checksum1, \
 *        method_id11,method_id12...,class_id1,class_id2...
 *    dex_location2,number_of_methods2,number_of_classes2,dex_location_checksum2, \
 *        method_id21,method_id22...,,class_id1,class_id2...
 *    .....
 **/

profile文件 的查看:

xxxx:/data/misc/profiles/cur/0/com.***.home # profman --profile-file=primary.prof --dump-only                                                                                                           
=== profile ===
ProfileInfo:
XXXHome.apk
    methods: 1824,1837,1843,1846,1907,1908,......
    classes: 62,63,64,68,69,74,75,77,79,83,86,......

其中:

XXXHome.apk表示 dex文件的位置,如果这个apk中有多个dex,比如 classes.dex 和 classes2.dex,则classes2.dex中的类,则以 XXXHome.apk:classes2.dex 命名。

methods 和 classes 后面的数据,表示他们在dex文件中的index。

我们使用profile模式 dex2oat编译时,会只编译profile中记录的这些 class 和 methods。

App-image 文件

/data/app/com.facebook.katana-1/oat/arm/base.art
/data/app/com.facebook.katana-1/oat/arm/base.odex

base.art就是对应的 app-image文件。

base.art文件主要记录已经编译好的类的具体信息以及函数在oat文件的位置,相当于缓存,在app运行的时候会加载到虚拟机,可以加快启动速度。

app-image文件是Android7.0之后,ART使用的文件,它是App使用的类以及函数数据的缓存,在app启动的使用mmap到内存空间,以加快app启动速度,App启动过程越复杂,使用app-image时的提升越明显

App Images: We use the start up classes to build a pre-populated heap where the classes are pre-initialized (called an app image). When the application starts, we map the image directly into memory so that all the startup classes are readily available.

The benefit here is that the app’s execution saves cycles since it doesn’t need to do the work again, leading to a faster startup time.

app-image如何使用

app-image在App启动的过程中加载,加载流程如下:

参考应用启动流程

->ActivityThread.main()
->...
->ActivityThread.performLaunchActivity()
->ActivityClientRecord.packageInfo.getClassLoader()
->LoadedApk.getClassLoader()
->LoadedApk.createOrUpdateClassLoaderLocked()
->ApplicationLoaders.getDefault().getClassLoader()
->new PathClassLoader()
->new BaseDexClassLoader()
->new DexPathList()
->makePathElements
->loadDexFile
->new DexFile()
->openDexFile()
->openDexFileNative
->openDexFilesFromOat()
->OpenImageSpace(source_oat_file)// 在这里尝试打开oat文件对应的image文件,
-> heap ->AddSpace(image_space);
-> class_linker ->AddImageSpace(image-space)

class_linker的 AddImageSpace中会调用 UpdateAppImageClassLoadersAndDexCaches()方法:

bool ClassLinker::UpdateAppImageClassLoadersAndDexCaches(
    ...
    ClassTable* table = InsertClassTableForClassLoader(class_loader.Get());
    for (size_t i = 0; i < num_dex_caches; i++) {
      mirror::DexCache* const dex_cache = dex_caches->Get(i);
      const DexFile* const dex_file = dex_cache->GetDexFile();
      RegisterDexFileLocked(*dex_file, hs3.NewHandle(dex_cache));
      GcRoot<mirror::Class>* const types = dex_cache->GetResolvedTypes();
      const size_t num_types = dex_cache->NumResolvedTypes();
        for (int32_t j = 0; j < static_cast<int32_t>(num_types); j++) {
          mirror::Class* klass = types[j].Read();
          if (space->HasAddress(klass)) {
            klass->SetClassLoader(class_loader.Get());
          }
          table->Insert(klass);
        }
      ...

==在这个函数中,会把app-image中的所有类的 classLoader更新为当前的 classLoader,并将它们添加到当前的ClassLoader的class table中;之后在当前进程中有使用相关类时,在FindClass过程中,直接就能在 class table中找到,即可使用,免去了类的加载。至此,app进程在后续的运行中,就可以直接使用app-image中的类了==。

oatdump

xxx:/data/dalvik-cache/arm # oatdump --app-image=system@priv-app@Browser@Browser.apk@classes.art --app-oat=system@priv-app@Browser@Browser.apk@classes.dex --image=/system/framework/boot.art --instruction-set=arm --header-only

获取完整数据可以把 header-only 参数去掉即可。

dex2oat

dex2oat 是什么

dex2oat是一个可执行程序,在手机的 /system/bin/dex2oat,它的作用是编译dex文件,生成oat文件。

dex文件被编译为 oat文件的过程,就是由 /system/bin/dex2oat 程序触发的; 而实际上编译业务是在 libart-compiler.so中做的。

== dex2oat(dex文件) => oat文件/image文件==

dex2oat 什么时候被触发

dex2oat进程的启动,可以分为两大类:一类是 installd进程触发的dex2oat;另一类是由 app中直接调用的 dex2oat。

installd 中触发的 dex2oat

有以下几个场景:

1.应用安装,(包括普通安装和通过shellCmd安装),安装一个app时,安装过程中需要编译dex文件,会通知installd来触发一个dex2oat进程;

2.开机扫描,开机过程中,PMS扫描已安装app过程,判断需要优化时,则会对install发出通知;

3.BackgroundDexOptService,(空闲时段或者开机之后触发的Backgroud的 Job),会通知installd进行dex2oat;

4.OTADexoptService,好象是OTA过程中的触发的,这个场景没有进行过实际的验证;

app中调用 dex2oat

一般是App的进程fork出一个子进程,子进程用来执行dex2oat,编译相关的dex,而父进程进行 waitpid 等待,等待完成后再运行其他逻辑。

比如:

1.微信安装后的首次启动,是有dex2oat的调用

2.淘宝安装后的首次搜索,也有dex2oat的调用

这个也是其首次启动或者搜索的一个耗时点。

dex2oat生成oat和app-image文件

dex2oat --dex-file=/data/app/com.facebook.katana-1/base.apk --app-image-file=/data/app/com.facebook.katana-1/oat/arm/base.art --oat-file=/data/app/com.facebook.katana-1/oat/arm/base.odex --instruction-set=arm --instruction-set-variant=kryo --instruction-set-features=default --runtime-arg -Xms64m --runtime-arg -Xmx512m --compiler-filter=interpret-only --image-format=lz4 --runtime-arg -classpath --runtime-arg /system/framework/com.google.android.maps.jar

三段式编译模型

image

图2 基于LLVM架构开发的编译器执行过程

image

图3 利用现成的与语言无关的优化器和后端为语言相关的前端生成各种体系结构相关的机器指令

ELF格式的oat文件

image

图4 ART翻译classes.dex后得到的ELF格式的oat文件

参考

Other site

Android profile-guided dex2oat

https://android-developers.googleblog.com/2019/04/improving-app-performance-with-art.html

Android运行时ART简要介绍和学习计划

Android运行时ART加载OAT文件的过程分析

dex2oat源码流程分析

ART世界探险(16) - 快速编译器下的方法编译

image