按时间顺序开始介绍吧

梳理一下各个 Android 版本的 虚拟机和编译策略

1. Dalvik or ART?

Android 4.4 以前用的是 ​​Dalvik​​ 虚拟机,Android 4.4 开始引入 ART 虚拟机。

Android 4.4 版本上两种运行时环境共存,可以相互切换。

Android 5.0 之后,​​Dalvik​​ 虚拟机被彻底丢弃,全部采用 ART。

2. Android 诞生之初 ——> 单纯的 Dalvik

虽然Android 平台使用 ​​Java​​​ 语言来开发应用程序,但Android程序却不是运行在标准Java虚拟机上的。可能是为了解决移动设备上软件运行效率的问题,也可能是为了规避与Oracle公司的版权纠纷。Google为Android平台专门设计了一套虚拟机来运行Android程序,它就是 ​​Dalvik Virtual Machine(Dalvik虚拟机)​​。

​Dalvik​​​ 负责加载 ​​dex/odex​​ 文件并解析成机器码交由系统调用。

2.1 Dalvik虚拟机概述

Google于2007年底正式发布了Android SDK,Dalvik虚拟机也第一次进入了人们的视野。它的作者是丹·伯恩斯坦(Dan Bornstein),名字来源于他的祖先曾经居住过的名叫Dalvik的小渔村。Dalvik虚拟机作为Android平台的核心组件,拥有如下几个特点:

  • 体积小,占用内存空间小
  • 专有的DEX可执行文件格式,体积更小,执行速度更快
  • 常量池采用32位索引值,寻址类方法名、字段名、常量更快;
  • 基于寄存器架构,并拥有一套完整的指令系统;
  • 提供了对象生命周期管理、堆栈管理、线程管理、安全和异常管理以及垃圾回收等重要功能;
  • 所有的Android程序都运行在Android系统进程里,每个进程对应着一个Dalvik虚拟机实例

2.2 Dalvik虚拟机与Java虚拟机的区别

  • Java虚拟机运行的是Java字节码,Dalvik虚拟机运行的是Dalvik字节码
  • Dalvik虚拟机运行的是Dalvik字节码,所有的Dalvik字节码由Java字节码转换而来,并被打包到一个DEX(Dalvik Executable)可执行文件中
  • Dalvik可执行文件体积更小
  • dx的工具负责将Java字节码转换为Dalvik字节码。dx工具对Java类文件重新排列,消除在类文件中出现的所有冗余信息
  • Java虚拟机与Dalvik虚拟机架构不同
  • Java虚拟机基于栈架构,Dalvik虚拟机基于寄存器架构

2.2.1 基于寄存器架构的优点

  • JVM 基于栈架构。程序在运行时虚拟机需要频繁的从栈上读取或写入数据,这个过程需要更多的指令分派与内存访问次数,会耗费不少CPU时间,对于像手机设备资源有限的设备来说,这是相当大的一笔开销。
  • Dalvik虚拟机基于寄存器架构。数据的访问通过寄存器间直接传递,这样的访问方式比基于栈方式要快很多。

Dalvik设计之初是为了运行在嵌入式设备上,对性能要求比较高,并且对跨平台没有多大要求,因此Dalvik使用寄存器来加快代码的执行速度。

2.2.2 为什么使用 .dex 文件

Android SDK中有一个叫 ​​dx​​​ 的工具负责将 ​​Java​​​ 字节码转换为 ​​Dalvik​​​ 字节码 --> ​​.dex​​。

Android使用 ​​Dex​​​ 文件来代替 ​​Java​​​ 虚拟机的 ​​class​​​ 文件,相比于 ​​class​​​ 文件,​​Dex​​ 文件有以下的改进:

  • Dalvik可执行文件体积更小
  • Dex 文件格式是专为 Dalvik 设计的一种压缩格式。所以可以简单的理解为:Dex 文件是很多 .class 文件处理后的产物,最终可以在 Android 运行时环境执行
  • 由于dx工具对常量池的压缩,使得相同的字符串、常量在DEX文件中只出现一次,从而减小了文件的体积
  • 由于生成的代码指令减少了,程序执行速度会更快一些
  • Dex文件的签名只有一份,验证也只有一次
  • dex文件有个天大的好处:可以直接用DexClassLoader类加载,这叫动态加载。于是我们只要在dex上加壳,在程序运行时脱壳,就可以规避静态反编译的风险

2.3 为什么用 Dalvik 而不是传统的 JVM?

Google为什么不用 ​​JVM​​ 来当做 android 虚拟机?原因是版权和效率问题:

  • 为了解决移动设备上软件运行效率的问题
  • 也可能是为了规避与Oracle公司的版权纠纷

2.3.1 Dalvik 相对于 JVM 在 Android 上的优势

  • Dalvik 运行 .dex 文件,文件体积更小,执行速度更快
  • Dalvik虚拟机基于寄存器架构
  • 由于生成的代码指令减少了,程序执行速度会更快一些

3. Android 2.2 ——> JIT 首次登场

为了适应硬件速度的提升,Android 系统系统也在不断更新,单一的 Dalvik 虚拟机已经渐渐地满足系统的要求了,2010 年 5 月 20 日,Google 发布 Android 2.2(Froyo冻酸奶),在这个版本中,Google 在 Android 虚拟中加入了 JIT 编译器:​​Just-In-Time Compiler​

​Dalvik​​​ 虚拟机可以看做是一个 ​​Java VM​​,他负责解释dex文件为机器码,如果我们不做处理的话,每次执行代码,都需要Dalvik将dex代码翻译为微处理器指令,然后交给系统处理,这样效率不高。为了解决这个问题,Google在2.2版本添加了JIT编译器,当App运行时,每当遇到一个新类,JIT编译器就会对这个类进行编译,经过编译后的代码,会被优化成相当精简的原生型指令码(即native code),这样在下次执行到相同逻辑的时候,速度就会更快。

JIT(Just-in-time Compilation,即时编译),又称为动态编译,是一种通过在运行时将字节码翻译为机器码的技术,使得程序的执行速度更快

官方宣称 ​​JIT​​​ 的引入使得 ​​Dalvik​​ 的性能提升了 3~6 倍。

3.0 JIT 编译方式

主流的JIT包含两种字节码编译方式:

  • method方式:以函数或方法为单位进行编译。
  • trace方式:以trace为单位进行编译。

那什么是 ​​trace​​​ 方式呢?在函数中一般很少是顺序执行代码的,多数的代码都分成了好几条执行路径,其中函数的有些路径在实际运行过程中是很少被执行的,这部分路径被称为“冷路径”,而执行比较频繁的路径被称为“热路径”。采用传统的 ​​method​​ 方式会编译整个方法的代码,这会使得在“冷路径”上浪费很多编译时间,并且耗费更多的内存;

​trace​​方式编译则能够快速地获取“热路径”代码,使用更短的时间与更少的内存来编译代码。

目前,Dalvik虚拟机默认采用trace方式编译代码

3.1 JIT 优点

  • 安装速度超快 存储空间小
  • 每次应用在运行时,它实时的将一部分 dex翻译成机器码。在程序的执行过程中,更多的代码被被编译并缓存。由于 JIT 只翻译一部分代码,它消耗的更少的内存,占用的更少的物理存储空间

3.2 JIT 的缺点

但是 JIT 模式的缺点也不容忽视:

  • 运行时比较耗电,造成电池额外的开销
  • JIT中需要解释器,解释器解释的字节码会带来CPU和时间的消耗 由于热点代码的Monitor一直在运行,也会带来电量的损耗
  • Android SDK < 21, 安装或者升级更新之后,首次冷启动的耗时漫长
  • Multidex加载的时候会非常慢,因为Dalvik 虚拟机只能执行做过 OPT 优化的 DEX 文件,也就是我们常说的 ODEX 文件
  • 由于在Dex加载时会触发dexopt , 导致Multidex加载的时候会非常慢

Android 虚拟机进化史_存储空间

3.3 Dalvik虚拟机是如何执行程序的

Android系统的架构采用分层思想,这样的好处是拥有减少各层之间的依赖性、便于独立分发、容易收敛问题和错误等优点。

Android系统由Linux内核、Libraries、Android Runtime、应用程序框架以及应用程序组成。

Dalvik虚拟机属于Android运行时环境,它与一些核心库共同承担Android应用程序的运行工作

Android 虚拟机进化史_存储空间_02

3.3.1 Android 进程如何创建出来的

  • Android系统启动加载完内核后,第一个执行的是init进程,init进程首先要做的是设备的初始化工作,然后读取init.rc文件并启动系统中的重要外部程序Zygote。
  • Zygote进程是Android所有进程的孵化器进程,它启动后会首先初始化Dalvik虚拟机,然后启动system_server并进入Zygote模式,通过socket等候命令。
  • 当执行一个Android应用程序时,system_server进程通过socket方式发送命令给Zygote,Zygote收到命令后通过fork自身创建一个Dalvik虚拟机的实例来执行应用程序的入口函数,这样一个程序就启动完成了。

Android 虚拟机进化史_android_03


Zygote提供了三种创建进程的方法:

  • fork(),创建一个Zygote进程;
  • forkAndSpecialize(),创建一个非Zygote进程;
  • forkSystemServer(),创建一个系统服务进程。

其中,​​Zygote​​​ 进程可以再 ​​fork()​​​ 出其他进程,​​非Zygote进程​​​ 则不能 ​​fork​​​ 其他进程,而 ​​系统服务进程​​ 在终止后它的子进程也必须终止。

3.3.2 Dalvik虚拟机执行程序流程

当进程fork成功后,执行的工作就交给了Dalvik虚拟机。

  • Dalvik虚拟机首先通过loadClassFromDex()函数完成类的装载工作,每个类被成功解析后都会拥有一个ClassObject类型的数据结构存储在运行时环境中,虚拟机使用gDvm.loadedClasses全局哈希表来存储与查询所有装载进来的类
  • 随后,字节码验证器使用dvmVerifyCodeFlow() 函数对装入的代码进行校验
  • 接着虚拟机调用FindClass() 函数查找并装载main方法类
  • 随后调用dvmInterpret() 函数初始化解释器并执行字节码流。
  • Android 虚拟机进化史_存储空间_04

4. Andorid 4.4 ——> 引入 ART 和 AOT

2013 年 10 月 31 日,Google 发布 Android 4.4 ​​Kitkat​​​,带来了全新的虚拟机运行环境 ​​ART​​:Android RunTime 的预览版和全新的编译策略 AOT(Ahead-of-time)

Android Runtime (ART) 是 Android 上的应用和部分系统服务使用的托管式运行时。ART 及其前身 Dalvik 最初是专为 Android 项目打造的。作为运行时的 ART 可执行 Dalvik 可执行文件并遵循 Dex 字节码规范。

ART 和 Dalvik 是运行 Dex 字节码的兼容运行时,因此针对 Dalvik 开发的应用也能在 ART 环境中运作。不过,Dalvik 采用的一些技术并不适用于 ART。

需要注意的是,这时期 ​​ART​​​ 是和 ​​Dalvik​​ 共存的,用户可以在两者之间进行选择(感觉怪怪的,用户可是小透明啊)

4.1 ART 主要功能

4.2 AOT 编译模式的特点

  • 优点 : 运行时省电 , 运行速度快
  • 缺点 :
  • 由于安装APK时触发dex2oat , 需要编译成native code , 导致安装时间过长
  • 由于dex2oat生成的文件较大 , 会占用较多的空间

5. Android 5.0 ——> 全面使用 ART + AOT

5.1 ART 的功能改进

参考 ​​Android 8.0 中的 ART 功能改进​​:

AOT 模式解决了应用启动和运行速度和耗电问题的同时也带来了另外两个问题:

  • 应用安装和系统升级之后的应用优化比较耗时
  • 优化后的文件会占用额外的存储空间

Android 虚拟机进化史_字节码_05

2014 年 10 月 16 日,Google发布Android 5.0:Lollipop,ART 全面取代 Dalvik 成为 Android 虚拟机运行环境,至此,Dalvik 退出历史舞台,AOT 也成为唯一的编译模式。

AOT 和 JIT 的不同之处在于:

  • JIT 是在运行时进行编译,是动态编译,并且每次运行程序的时候都需要对 odex 重新进行编译
  • AOT 是静态编译,应用在安装的时候会启动 dex2oat 过程把 dex 预编译成 ELF 文件,每次运行程序的时候不用重新编译,是真正意义上的本地应用
  • Android 虚拟机进化史_android_06

6. Android 7.0 ——> JIT 回归

在 Android 5.x 和 6.x 的机器上,系统每次 OTA 升级完成重启的时候都会有个应用优化的过程,这个过程就是 ​​dex2oat​​ 过程,这个过程比较耗时并且会占用额外的存储空间。

2016年8月22日,Google发布Android 7.0(牛轧糖Nougat),JIT 编译器回归,形成 ​​AOT/JIT​​ 混合编译模式,这种混合编译模式的特点是:

  • 应用在安装的时候 dex 不会被编译
  • 应用在运行时 dex 文件先通过解析器(Interpreter)后会被直接执行(这一步骤跟 Android 2.2 - Android 4.4之前的行为一致),与此同时,热点函数(Hot Code)会被识别并被 JIT 编译后存储在 jit code cache 中并生成 profile 文件以记录热点函数的信息。
  • 手机进入 IDLE(空闲) 或者 Charging(充电) 状态的时候,系统会扫描 App 目录下的 profile 文件并执行 AOT 过程进行编译。

可以看出,混合编译模式综合了 AOT 和 JIT 的各种优点,使得应用在安装速度加快的同时,运行速度、存储空间和耗电量等指标都得到了优化

Android 虚拟机进化史_android_07

7. 问题

7.1 Android 7.0(Android N) 为什么安装速度更快?所需存储空间更小

安装速度变快

在Android N中,应用在安装时不再做编译,而是解释字节码。省去了冗长的编译时间,安装速度自然大大提升。

这就要归功于新加入的这个 ​​JIT/AOT​​ 混合编译技术了。新增的JIT编译器用于对ART进行代码分析,使之可以在应用运行时,持续优化Android应用的性能。使得安装时不做编译,也能达到与安装时完整编译一样的效果。这种编译模式我们依旧同称为AOT,只不过它的含义不再是预编译,而是全时编译技术(All Of the Time compilation)

此外,JIT的分析结果会被保存起来。当Android设备空闲或充电时,ART就会根据JIT的分析结果,将代码中的常用方法进行编译,而不常用的方法则待到需要时再编译,因而省下了部分存储空间。直观的体现就是我们安装完应用后,存储空间的占用变少了。

应用占用空间对比

根据实测,手机淘宝6.5.0安装完后在Android M中占用空间为171MB,而Android N中占用空间为156MB。
王者荣耀1.17.1.23安装完后在Android M中占用空间为439MB,在Android N中占用空间为428MB。
以下为个人推测:
王者荣耀在两个版本中的占用空间差没有手机淘宝的大的原因,很可能是因为手游中多为常用的交互代码,而手机淘宝中大多数交互都能在web中完成,因而不常用的代码可能更多。造成了这个现象。

总结

Android N安装应用快,是因其在安装时只解释字节码,省去了编译所用的时间。之所以能省去编译环节,是因其加入了全新的AOT全时编译技术,使得应用执行效率保持与Android M相同甚至更好。因而ART不再编译所有代码,所以省下了部分存储空间。

7.2 dexopt 与 dex2oat 的区别

Android 虚拟机进化史_android_06


前者针对 Dalvik 虚拟机,后者针对 Art 虚拟机。

  • dexopt 是对 dex 文件 进行 verification 和 optimization 的操作,其对 dex 文件的优化结果变成了 odex 文件,这个文件和 dex 文件很像,只是使用了一些优化操作码(譬如优化调用虚拟指令等)。
  • dex2oat 是对 dex 文件的 AOT 提前编译操作,其需要一个 dex 文件,然后对其进行编译,结果是一个本地可执行的 ELF 文件,可以直接被本地处理器执行。

除此之外在上图还可以看到 Dalvik 虚拟机中有使用 JIT 编译器,也就是说其也能将程序运行的热点 java 字节码编译成本地 code 执行,所以其与 Art 虚拟机还是有区别的。

Art 虚拟机的 dex2oat 是提前编译所有 dex 字节码,而 Dalvik 虚拟机只编译使用启发式检测中最频繁执行的热点字节码

7.3 Android各版本虚拟机 dexopt 产物的区别

5.0以下

使用Dalvik虚拟机 , 生成 ​​odex​​ 文件 . Dalvik采用的是JIT编译+解释器,也就是即时编译,每次应用运行时会实时将Dex翻译成机器码.

  • 优点 : 安装速度超快 , 占用存储空间小
  • 缺点 :
  • 由于在Dex加载时会触发dexopt , 导致Multidex加载的时候会非常慢
  • 由于热点代码的Monitor一直在运行 , 解释器解释的字节码会带来CPU和时间的消耗, 会带来电量的损耗

5.0 - 7.0

使用ART虚拟机 , 生成 ​​oat​​​ 文件. 在ROM OTA或者恢复出场设置后 , 会要进行 ​​dex2oat​​​ 根据当前ROM进行重新编译生成 ​​.oat​​ 文件.

  • 优点 : 运行时省电 , 运行速度快
  • 缺点 :
  • 由于安装APK时触发dex2oat , 需要编译成native code , 导致安装时间过长
  • 由于dex2oat生成的文件较大 , 会占用较多的空间

7.0 - 8.0

使用ART虚拟机 , 但是在7.0之上 , 增加了​​.vdex​​​ 与 ​​.art​​​ 机制 , 在 ​​ART​​​ 虚拟机再次启动/升级 , 加载 ​​Dex/Oat​​​ 文件时 , 会减少 ​​Dex​​ 的校验时间 , 提升加载与运行效率

9.0

在 ​​ART​​​ 虚拟机的基础上 , 增加了 ​​Cdex ( Compat Dex )​​机制 ,

Compiler-fileter

在dex2oat的时候 , 会有一个目标编译类型 , 会有以下几类 , 根据时机不同dex2oat的编译方式也会不同

  • verify:只运行 DEX 代码验证。
  • quicken:运行 DEX 代码验证,并优化一些 DEX 指令,以获得更好的解译器性能。
  • speed:运行 DEX 代码验证,并对所有方法进行 AOT 编译。
  • speed-profile:运行 DEX 代码验证,并对配置文件中列出的方法进行 AOT 编译。

8. odex、vdex、cdex、art 文件

Android 虚拟机进化史_字节码_09

8.0 dex

​dex(Dalvik VM Excutors)​​​:​​Dalvik​​ 虚拟机执行程序,执行前需要优化

8.1 vdex

Android 8.0(Android O) 在 ​​odex​​​ 的基础上又引入了 ​​vdex​​​ 机制,目的是为了避免不必要的验证 ​​dex​​​ 文件合法性以降低 ​​dex2oa​​ t时间

因为当系统ota后,用户自己安装的应用是不会发生任何变化的,但framework代码已经发生了变化,

所以就需要重新对这些应用也做dex2oat,所以如果有 ​​vdex​​​ 的话,就可以省去重新校验 ​​apk​​​ 里 ​​dex​​ 文件合法性的过程,节省一部分时间

主要目的:降低dex2oat执行耗时

  • 当系统OTA后,对于安装在data分区下的app,因为它们的apk都没有任何变化,那么在首次开机时,对于这部分app如果有vdex文件存在的话,执行dexopt时就可以直接跳过verify流程,进入compile dex的流程,从而加速首次开机速度;
  • 当app的jit profile信息变化时,background dexopt会在后台重新做dex2oat,因为有了vdex,这个时候也可以直接跳过

原理:

  • 应用首次安装时,抽取出其中的dex文件,校验成功后,存储到一个独立的文件中,后面由于jit profile改变,或OTA等原因,而重新进行dexopt时,可以跳过dex文件校验流程

8.2 odex

在Android N 之前,Dalvik虚拟机执行程序 ​​dex​​​文件前,系统会对dex文件做优化,生成可执行文件 ​​odex​​​,保存到 ​​data/dalvik-cache​​ 目录,最后把apk文件中的dex文件删除。

优点:

  • 减少了启动时间(省去了系统第一次启动应用时从apk文件中读取dex文件,并对dex文件做优化的过程。)和对RAM的占用(apk文件中的dex如果不删除,同一个应用就会存在两个dex文件:apk中和​​data/dalvik-cache​​ 目录下)
  • 防止第三方用户反编译系统的软件(odex文件是跟随系统环境变化的,改变环境会无法运行;而apk文件中又不包含dex文件,无法独立运行)。

在Android O 之后,odex 是从vdex 这个文件中 提取了部分模块生成的一个新的 可执行二进制码 文件 , odex 从vdex 中提取后,vdex 的大小就减少了。

  • 第一次开机就会生成在​​/system/app/<packagename>/oat/​​ 下
  • 在系统运行过程中,虚拟机将其 从​​/system/app​​ 下 copy 到 ​​/data/davilk-cache/​​ 下
  • odex + vdex = apk 的全部源码 (vdex 并不是独立于odex 的文件 odex + vdex 才代表一个apk )

8.3 .art

​odex​​​ 进行优化生成的可执行二进制码文件,主要是apk 启动的常用函数相关地址的记录,方便寻址相关; 通常会在 ​​data/dalvik-cache/​​ 保存常用的jar包的相关地址记录。

  • 第一次开机不会生成在/system/app//oat/ 下,以后也不会;
  • odex 文件在运行时,虚拟机会计算函数调用频率,进行函数地址的修改, 最后在​​/data/davilk-cache/​​ 由虚拟机生成;
  • 生成art 文件后,​​/system/app​​ 下的 ​​odex​​ 和 ​​vdex​​ 会无效,即使你删除,apk也会正常运行
  • push 一个新的apk file 覆盖之前/system/app 下apk file ,会触发PKMS 扫描时下发force_dex flag ,强行生成新的vdex 文件 ,覆盖之前的vdex 文件,由于某种机制,这个新vdex 文件会copy到​​/data/dalvik-cache/​​ 下,于是​​art​​文件也变化了。

8.4 oat

ART虚拟机使用的是 ​​oat​​​文件,​​oat​​​ 文件是一种Android私有 ​​ELF​​文件格式,它不仅包含有从DEX文件翻译而来的本地机器指令,还包含有原来的DEX文件内容。

APK在安装的过程中,会通过 ​​dex2oat​​​ 工具生成一个​​OAT​​文件。对于apk来说,oat文件实际上就是对odex文件的包装,即 ​​oat=odex​

参考链接