Android DEX优化

为详细了解Android App在安装之后,代码在Android Dalvik虚拟机中执行的过程,本文将讲述Android dex的优化过程。在学习Android dex的过程中需要解决几个问题:

  1. Android DEX的是如何产生的?
  2. Android DEX文件结构简介?
  3. Android为什么要进行DEX Opt?
  4. Dex Opt什么时候进行?
  5. Android DEX Opt主要做了什么操作?
  6. Art出现虚拟机之后,对Android DEX执行过程的影响?
  7. Android MultiDex技术对Dex优化有何影响?

1. Android DEX简介

Android DEX(Dalvik VM executes)文件是Android Dalvik虚拟机可执行文件,该文件是由Android工程中Java类的字节码文件的集合。熟悉DEX文件结构、了解Android DEX生成过程,是学习Android dex优化、Android APK加固技术、Android multiDex等技术的基础。本节便主要介绍Android DEX的文件结构、Android DEX文件产生过程、以及Android DEX与Java Class文件的异同点等。

1.1 Android DEX文件结构

1.1.1 Android文件结构概览

整个DEX文件的结构如下图所示:
![DEX文件结构](dex.png)

1.1.2 DEX文件Header

DEX文件Header的字节结构如下表所示: |字段名称|偏移值|长度|数据类型|描述| |---|:---:|---|:---|---| |magic| 0x0|4 |ubyte[4]|魔数用来标识dex文件dex\n| |version| 0x4| 4| ubyte[4]| 版本号,有035\0,036\0两种| |checksum| 0x8| 4| uint| 确保dex文件没有被损坏| |signature| 0xC| 20| ubyte[20]| 用SHA-1算法生成的签名| |file_size| 0x20| 4| uint| 文件的总大小| |header_size|0x24| 4| uint| 头部大小,固定为0x70字节| |endian_tag|0x28| 4| uint| 标识是大顶端还是小顶端| |link_size| 0x2C| 4| uint| 连接段的大小,为0表示不是静态链接| |link_off|0x30| 4| uint| 连接段从文件头部开始的偏移值| |map_off|0x34| 4| uint| 表示map数据离文件开头的偏移| |string_ids_size|0x38| 4| uint| 字符串地址列表中元素的个数| |string_ids_off|0x3C| 4| uint| 字符串列表离文件头的偏移| |type_ids_size| 0x40| 4| uint| 类型列表中类型的个数| |type_ids_off| 0x44| 4| uint| 类型列表离文件头的偏移| |proto_ids_size|0x48| 4| uint| 原型列表里原型的个数| |proto_ids_off| 0x4C| 4| uint| 类型列表离文件头的偏移| |field_ids_size|0x50| 4| uint| 字段列表中字段个数| |field_ids_off| 0x54| 4| uint| 字段列表离头文件的偏移| |method_ids_size|0x58| 4| uint| 方法列表中方法个数| |method_ids_off|0x5C| 4| uint| 方法列表离文件头的偏移| |class_defs_size|0x60| 4| uint| 类定义列表中类的个数| |class_defs_off|0x64| 4| uint| 类定义列表离文件头的偏移| |data_size| 0x68| 4| uint| 数据段的大小,必须以4字节对齐| |data_off| 0x6C| 4| uint| 数据段里文件头的偏移|

1.1.3 flyflow DEX文件

百度手机浏览器为解决DEX文件64K方法数的限制,采用MultiDex方案,将DEX文件拆分为三个DEX文件。其中class2.dex文件的header如下图所示:
![browser-class2-dex-header](flyflow-class2-dex-header.png)

1.2 APK文件的产生过程

博客 [android Apk打包过程概述](http://blog.csdn.net/jason0539/article/details/44917745) 详细介绍了APK打包过程,整个过程归结如下: 1. 打包资源文件,生成R.java文件 2. 处理aidl文件,生成相应java 文件 3. 编译工程源代码,生成相应class 文件 4. 转换所有class文件,生成classes.dex文件 5. 将dex文件和资源文件等一起打包生成apk文件 6. 对apk文件进行签名 7. 对签名后的apk文件进行对其处理

1.2.1 APK构建过程

Andorid APK构建的过程,简单描述如下图所示,该图来自 [Configure your build](https://developer.android.com/studio/build/index.html#build-process):
![Android APK Build Process](build-process_2x.png) APK构建的详细过程流如下图所示(出自[android Apk打包过程概述](http://blog.csdn.net/jason0539/article/details/44917745)):
![APK打包过程工作流图](apk-package.png)

1.2.2 Android APK构建相关工具介绍

整个过程中涉及的工具如下表所示: |工具名称|功能简介|工具所在位置|备注说明| |---|---|---|---| |aapt(Android Asset Package Tool)|Android资源打包工具|${ANDROID_SDK_HOME}/build-tools/ANDROID_VERSION/aapt|[Android aapt](http://elinux.org/Android_aapt)| |aidl(android interface definition language)|将aidl转化为.Java文件的工具|${ANDROID_SDK_HOME}/build-tools/ANDROID_VERSION/aidl|[aidl](https://developer.android.com/guide/components/aidl.html)| |javac|Java Compiler|${JDK_HOME}/javac或/usr/bin/javac|[javac usage](http://www.codejava.net/java-core/tools/using-javac-command)| |dex|转化.class文件为Davik VM能识别的.dex文件|${ANDROID_SDK_HOME}/build-tools/ANDROID_VERSION/dx|*| |apkbuilder|生成apk包|${ANDROID_SDK_HOME}/tools/apkbuilder|*| |jarsigner|.jar文件的签名工具|${JDK_HOME}/jarsigner或/usr/bin/jarsigner|[jarsigner](https://developer.android.com/studio/publish/app-signing.html#signing-manually)| |zipalign|字节码对齐工具|${ANDROID_SDK_HOME}/tools|[zipalign usage](https://developer.android.com/studio/command-line/zipalign.html)| 使用命令行编译Android APK可以参考博客[Building Android programs on the command line](http://geosoft.no/development/android.html)。

1.3 Class文件与DEX文件对比

本节讲述Class文件与DEX文件的异同点,两者的异同点如下表所示: |对比维度|Java Class文件|DEX文件| |---|---|---| |执行工具|JVM虚拟机,基于堆栈执行代码|Dalvik虚拟机,基于寄存器执行代码| |文件组成|一个Java类的字节码文件|文件包含若干个类的字节码,这些文件共享常量数据| |方法数限制|每个Class文件的方法上限为64K|即单个DEX文件的方法数限制为64K,即该文件中所有类的方法数之和为64K|

2 Android DEX优化

本节主要介绍Android进行DEX优化的动机,DEX优化的发生时机,DEX优化的过程,以及优化后的文件分析等。

2.1 Android DEX优化的原因

Dalvik虚拟机运行在RAM较小的系统中,数据被存放于速率较低的内部存储之中,这些限制使得DEX文件在设计时需要满足一下要求(本节内容翻译自[Dalvik Optimization and Verification With dexopt](http://www.netmite.com/android/mydroid/dalvik/docs/dexopt.html): 1. Class数据,尤其是字节码,必须在多个Process之间共享以便最小化整个系统内存的使用。(比如一个App可能会有多个Process)。 2. 启动一个新的App的开销必须最小化,以便设备保持响应。 3. 将类数据存放在私有文件中,会导致大量的冗余出现,特别是字符串常量。为了计生磁盘空间,我们需要考虑这个因素。 4. 解析类的数据域会增加在加载类的过程中的不必要的开销。直接访问数据值(比如整型和字符串类型)会效果更好。 5. 字节码验证是必须的,但是验证过程比较慢,所以尽量再App执行之外来验证字节码。 6. 字节码优化(加快指令和方法的裁剪)对于速度和电池寿命都有重要的影响。 7. 处于安全的考虑,进程不能编辑共享的代码。 出于以上需求,便得到以下几个基础的决定: 1. 多个Class文件被聚合到一个DEX文件; 2. DEX文件被映射到只读的内存中,并在多个Process之间共享; 3. 为适合本地的系统,会调整字节序列并进行字对齐操作; 4. 需要重写字节码的优化必须提前做(ahead of time); 5. 字节码验证是必须的,但是我们希望能够尽可能早的进行预验证。

2.2 Android优化过程

本节主要介绍Android DEX的优化过程

2.2.1 Android优化过程介绍

DEX优化主要完成以下任务(该内容源自博客[Dalvik Optimization and Verification With dexopt](http://www.netmite.com/android/mydroid/dalvik/docs/dexopt.html)): - 对于虚方法调用,把方法索引替换为vtable索引。 - 对于实例变量(field)的get/put,把变量索引替换为字节偏移。另外,把 boolean / byte / char / short 基本变量(variants)合并到单个的32位形式(解释器里更少的代码意味着CPU I-cache里更少的空间)。 - 替换一些高频次调用,比如把 String.length() 替换成”内联“的。这可以跳过一些常见的方法调用消耗,直接从解释器切换到native实现。 - 删除空方法。最简单的例子就是Object.,啥都没干,但却必须在任何对象被分配的时候执行。指令会被替换为一个新版本的空指令(no-op)形式,除非调试器被attach上去了。 - 附加预计算数据。例如,虚拟机想要一个类名的哈希表以便查找。不同于在加载DEX文件时候去计算这个,我们可以先计算,以节省堆(heap)空间和所有加载该DEX文件的虚拟机的计算时间。

2.2.2 Android DEX代码分析

本节通过分析DEX优化的核心代码来讲述DEX优化的过程: Android4.4版本的PackageManagerService的构造函数中有如下代码:
if (mSharedLibraries.size() > 0) {
    Iterator libs = mSharedLibraries.values().iterator();
    while (libs.hasNext()) {
    String lib = libs.next().path;
    if (lib == null) {
        continue;
    }
    try {
        if (dalvik.system.DexFile.isDexOptNeeded(lib)) {
            alreadyDexOpted.add(lib);
            mInstaller.dexopt(lib, Process.SYSTEM_UID, true);
            didDexOpt = true;
        }
    } catch (FileNotFoundException e) {
        Slog.w(TAG, "Library not found: " + lib);
    } catch (IOException e) {
        Slog.w(TAG, "Cannot dexopt " + lib + "; is it an APK or JAR? "+ e.getMessage());
    }
}
有上述代码可知,在PMS初始化的过程中,会调用Installd服务来完成dexopt优化工作。[Installd实现](https://android.googlesource.com/platform/frameworks/base/+/742a67127366c376fdf188ff99ba30b27d3bf90c/cmds/installd)dexopt的代码如下:
static void run_dexopt(int zip_fd, int odex_fd, const char* input_file_name,
    const char* dexopt_flags)
{
    static const char* DEX_OPT_BIN = "/system/bin/dexopt";
    static const int MAX_INT_LEN = 12;      // '-'+10dig+'\0' -OR- 0x+8dig
    char zip_num[MAX_INT_LEN];
    char odex_num[MAX_INT_LEN];
    sprintf(zip_num, "%d", zip_fd);
    sprintf(odex_num, "%d", odex_fd);
    execl(DEX_OPT_BIN, DEX_OPT_BIN, "--zip", zip_num, odex_num, input_file_name,
        dexopt_flags, (char*) NULL);
    ALOGE("execl(%s) failed: %s\n", DEX_OPT_BIN, strerror(errno));
}
该函数最终会调用execl来执行/system/bin/dexopt/命令来进行优化工作。 其他目录如framework目录等也是采用Shared Library同样的方式;系统其他预置的APK通过调用performBootDexopt来完成优化工作,这些函数最终也会调用Installd中的dexopt来完成优化工作。

dexopt实现。在dexopt实现代码中,指明了dexopt触发的时机:

  1. 来做VM的调用。该调用需要大量的参数,其中一个参数是file的描述符,该文件即作为输入文件也作为输出文件;
  2. 来自installd和其他Native的应用调用。传递的参数有:一个zip文件描述符、一个输出文件描述符、一个Debug消息的文件名。
  3. 在预优化构建的过程中会调用;

dexopt工具实现相关的类如下:

函数名称 函数功能
extractAndProcessZip 将class.dex从zip文件中解压出来,并保存到cache目录文件中,并为dexopt后的文件header预留一定空间
processZipFile 设备方面相关的处理以及预优化处理
fromZip 解析--zip参数
preopt 解析--preopt参数
fromDex 解析--dex参数,该函数最终会调用VM的DexOptimize类中的方法dvmContinueOptimization完成相关操作

DexOptimize类完成的操作有:

DexOptimize操作

3 ART虚拟机与DEX优化

Android 5.0引进了ART虚拟机,引入ART虚拟机后,主要发生了以下几个变化:

  1. Android APP文件在安装的时候,ART虚拟机会利用ahead of time技术将DEX文件编译成OAT文件,而不会再产生ODEX文件。
  2. Android5.0系统中/system/bin/dexopt被删除,取而代之是dex2oat.在对DEX文件进行处理时,dex2oat工具比dexopt工具更耗时(Configuring ART)
  3. Android5.0系统不存在/data/dalvik-cache/文件夹;
  4. Android ART虚拟机的引入和Dex的优化的目的都是为了加速APK的执行;

4 Multidex技术与DEX优化

众所周知,Android DEX文件存在一个64K方法数的限制.为解决方法数的问题,通常有以下几种方式:

  1. 加大Proguard的力度来减小DEX的大小和方法数,但这是治标不治本的方案,随着业务代码的添加,方法数终究会到达这个限制;
  2. 比较流行的方案是插件化方案:将一部分功能做成插件,采用动态加载的方式来加载。
  3. Android官方在Android5.0引入了multiDex方案,具体内容可参考Configure Apps with Over 64K Methods博客;
  4. google在推出MultiDex之前Android Developers博客介绍的通过自定义类加载过程
  5. Facebook推出的为Android应用开发的Dalvik补丁。 本节主要介绍MultiDex方案,主要包括该方案的步骤,该方案的不足之处,以及该方案对DexOpt的影响。

4.1 Multidex的主要步骤

本小节主要介绍使用Multidex方案解决Dex文件64K限制的问题,主要步骤包括:

  1. 修改Gradle的配置,支持multidex;
  2. 让应用支持多DEX文件。在官方文档中描述了三种可选方法(参考MultiDexApplication):
    • 在AndroidManifest.xml的application中声明android.support.MultiDex.MultiDexApplication,让该Application 作为App的Application;
    • 如果你已经有自己的Application类,让其继承MultiDexApplication;
    • 如果自己的Application继承了其他的Application类,可以Override自己的Application类的attachBaseContext方法。

4.2 Multidex方案存在的问题

使用Multidex方案解决dex方法数的过程中,会引入以下几个问题(参考Configure Apps with Over 64K Methods):

  1. Android Dex分为主要Dex(即为class.dex)和次要Dex之分;首次启动过程中,会进行次要Dex安装,如果次要的dex太大,会导致ANR的发生。
  2. 采用MultiDex方案的应用可能不能在低于Android 4.0 (API level 14) 机器上启动,这个主要是因为Dalvik linearAlloc的一个bug (Issue 22586);
  3. 采用MultiDex方案的应用因为需要申请一个很大的内存,在运行时可能导致程序的崩溃,这个主要是因为Dalvik linearAlloc 的一个限制(Issue 78035). 这个限制在 Android 4.0 (API level 14)已经增加了。
  4. 必须将启动过程中需要的类添加到Primary Dex中,否则会导致NoClassFoundError。 目前浏览器支持的SDK版本范围为14-23,因此主要面临的问题是问题1和问题4(具体参考Mulitdex优化).对于问题4的解决方案是,干预Android APK的编译过程,将启动过程中需要的类添加到main-dex-list文件中,以便dx.jar在根据main-dex-list打包主Dex。这两个问题的解决方案,以后会另文分析,这里不做详述。

4.3 Mutidex的安装过程

Multidex的安装,在Android5.0之前和之后表现不同:

  1. 在Android5.0(API level 21)版本之前,使用的是Dalvik虚拟机。Dalvik限制每个APK只有一个classes.dex字节码文件。为了解决这个问题,需要将multidex support library作为应用主Dex的一部分,然后有该library完成安装。(这个是本节分析的重点)。
  2. 在Android5.0版本之后,ART在native层支持从APK文件中加载多个dex文件。在安装App时ART会预编译App,并扫描所有的dex文件,将他们编译成一个oat文件。

在Android5.0之前版本,进行安装时,整个过程流程图如下(MultiDex源码): MultiDex安装过程

注:其中pathList字段定义在BaseDexClassLoader.java中。DexPathList定义了所有的Dex的路径(Dex加载相关源码)。

通过MultiDex Install过程分析,可以得到以下结论:

  1. Android SDK版本为4-20时,才会在Java层进行MultiDex安装。MultiDex安装过程发生在App首次启动过程中,该过程中发生中不存在Dex优化过程。也就是说对于SDK版本4-20的App,只有主Dex才会进行Dex优化操作。
  2. Android5.0以上版本系统,multiDex安装在native层进行;所有App都不会进行Dex优化操作。
  3. Android5.0以下版本在安装次要Dex的过程中,主要操作是将Dex负责到一个新的文件夹中,并将这些次要的Dex的Path的取值覆盖原有的Dex的Path的值。在DexOpt之后,/data/data-cache/目录下,只存在主Dex的优化结果,不存在其他的Dex的优化结果。

5 小结

本文简要介绍了以下的内容:

  1. Android dex文件结构;
  2. Android APK产生过程;
  3. Android Dex优化动机;
  4. Android Dex主要优化的内容;
  5. Android MultiDex的使用及原理初探;
  6. Android Dex加固技术的简介。

参考文献

  1. Dex文件结构
  2. dex2oat与oat文件的格式定义
  3. Android Dex文件解析之Dex文件头
  4. Android应用程序资源的编译和打包过程分析
  5. android Apk打包过程概述_android是如何打包apk的
  6. Configure Your Build
  7. Building Android programs on the command line
  8. Dalvik虚拟机简要介绍和学习计划
  9. Android逆向分析3
  10. dex分包变形记
  11. Optimizing Android bytecode with ReDex
  12. Android分包原理

results matching ""

    No results matching ""