1_NbI7Vp2R26gFW_maliOVSw.png

在最开始的时候,CPU 只能处理简单的数学运算,比如加减乘除。经过这么多年的发展,CPU 已经进化到可以处理非常复杂的运算,比如图片处理、音频解码等等。目前最知名的移动处理器是高通生产的骁龙系列。
但高通并不是唯一的 CPU 制造商。其他制造商生产的 CPU 有些架构与高通相同,有些却不一样。这里我要说:“欢迎来到地狱!”。如果你曾经开发过 C++/C,你就会知道 native code 需要为所有支持的架构编译一份,比如 ARM、ARM64、X86、X64、MIPS 等等。

作为一个 Android 开发者,通常你的应用需要支持多种多样的设备,而这些设备背后的 CPU 架构不尽相同。这基本上意味着你需要为每一种架构编译一个 so 文件。

JVM 完美解决了这个问题。JVM 在硬件上面添加了一层抽象。通过这种方式,你的应用程序就可以 通过 Java 的接口来使用 CPU ,而你也不用去为了不同的 CPU 架构做适配,也不用为了 Mac 上与众不同的蓝牙驱动而烦恼。

Android CPU,编译器,D8 & R8 - 图2

javac 编译器将你的 Java 代码编译为字节码(.class 文件),然后你的代码就可以直接在 Java 虚拟机上运行,而不用关心底层操作系统的差异。作为一个应用开发者,你不用去关心设备硬件、操作系统、内存、CPU 的差异,只需要关注业务逻辑。

JVM 内部

Android CPU,编译器,D8 & R8 - 图3

JVM 有三个主要的区域:

  1. ClassLoader - 主要职责是加载编译后的字节码(.class 文件),链接,检测损坏的字节码,定位并初始化静态变量和静态代码
  2. Runtime Data - 负责所有的程序数据:栈,方法变量,当然还有我们都非常熟悉的堆
  3. Execution Engine - 负责执行已经加载的代码并清理不在需要的垃圾(GC)

    Interpreter & JIT

这两个家伙在一起工作,每当我们运行我们的程序,解释器都需要将字节码解释为 机器码 再运行。这么做最主要的一个缺点就是当一个方法需要多次执行的时候,每次执行都需要进行解释。

JIT 编译器就是用来解决这个问题的。执行引擎还是使用解释器解析代码,但不同的是,当它发现有重复执行的代码时,它会切换为 JIT 编译器,JIT 编译器会将这些重复的代码编译为 本地机器代码,而当同样的方法再次被调用时,已经被编译好的本地机器代码就会被直接运行,从而提升系统的性能。这些重复执行的代码也被称为「热代码(Hot code)」。
Android CPU,编译器,D8 & R8 - 图4

Dalvik

Java 虚拟机的设计一直以来都是面向有无限电量和几乎无限存储的设备。

而 Android 设备则很不相同。首先电池容量有限,所有的程序都需要为了有限的资源竞争。其次内存的大小有限,存储空间也很有限(跟其他的 JVM 运行设备相比,简直是小的可怜)。因此,当 Google 决定在移动设备上使用 JVM 的时候,他们做了很多的改动 - 包括 java 代码编译为字节码的过程以及字节码的结构等等。

Android CPU,编译器,D8 & R8 - 图5 Android CPU,编译器,D8 & R8 - 图6

之所以有这样的区别是因为普通的 Java 字节码是以栈为基础的(所有的变量都存储在栈中),而 dex 格式的字节码是是 寄存器为基础的(所有的变量都存储在寄存器中)。后者更加高效并且需要更少的空间。运行 Dex 字节码的 Android 虚拟机被称为 Dalvik。
Android CPU,编译器,D8 & R8 - 图7

Android 构建系统

语雀内容

ART

Dalvik 曾经是一个很不错的解决方案,然而它也有不少局限性。所以呢,google 后来又推出了一个优化后的 Java 虚拟机,叫 ART。ART 与 Dalvik 的主要区别是,它不是在运行时进行解释和 JIT 编译,而是直接运行的提前编译好的 .oat 文件,因此获得了更好更快的运行速度。为了提前编译好 .oat 二进制文件,ART 使用了 AOT 编译器(AOT 是 Ahead of Time 的缩写)

那么,到底什么是 .oat 二进制文件呢?
Android CPU,编译器,D8 & R8 - 图8
当你从应用商店下载并安装一个应用的时候,除了解压缩 .apk 文件,系统也会对 .dex 文件进行编译,生成 .oat 文件。

所以当你点击应用图标的时候,ART 直接加载 .oat 文件并运行,而不需要任何的解释和 JIT 步骤。

  • 这会导致安装或更新应用的速度特别慢
  • 所有的 .dex 文件都会被编译为 .oat 文件,即使有些代码几乎不被用户使用

Google 的工程师想出了一个绝妙的点子来解决问题,充分利用了 Interpreter/JIT/AOT 的优点:

  1. 最开始安装的时候并没有 .oat 文件生成,当你第一次运行应用的时候,ART 会使用解释器来解释执行 .dex 代码
  2. 当 Hot Code 被发现的时候,ART 会调用 JIT 来对代码进行编译
  3. 使用 JIT 编译过的代码以及编译选项会存储在缓存中,以后每次执行同样的代码就会使用这里的缓存
  4. 当设备空闲的时候(屏幕熄灭并且在充电),所有的 Hot Code 会被 AOT 编译器使用缓存的编译选项编译为 .oat 文件
  5. 当你再次运行应用的时候,位于 .oat 文件的代码会被直接执行,从而获得更好的性能,而如果要执行的代码不在 .oat 文件中,则回到第一步

Android CPU,编译器,D8 & R8 - 图9

R8

Google 的老伙计们付出了巨大的努力来改进编译速度,实际上我们这些努力确实也收到了不错的效果,然而,Dalvik/ART 支持的 opcodes 是非常受限的。Java 7-8-9 等等新引入的语言特性并不能直接就能用在 Android 开发中,基本上现在的所有的 Android 开发者还在被困在 Java 6 SE 上。

为了让我们能使用上 Java 8 的特性,Google 使用了 Transformation 来增加了一步编译过程叫 desugaring,其实就是将我们代码里使用的 java 8 新特性翻译为 Dalvik/ART 能够识别的 java 6 字节码。这不可避免会导致一个问题 - 更长的编译时间

Android CPU,编译器,D8 & R8 - 图10

Dope8

为了解决这个问题,在 Android Studio 3.2 中,Google 使用 D8 替换了旧的 dx 编译器。D8 的主要改进是消除 desuguaring 的过程,让其成为 dex 编译的一部分,从而加快编译速度。

Android CPU,编译器,D8 & R8 - 图11

R8 替代 Proguard

Proguard 也是编译过程中一个 transformation 的步骤,当然也会影响编译时间。为了解决这个问题,R8 在 dex 过程中也会做类似的事情(比如优化、混淆、清理无用的类),而避免了多一步 transformation。

Android CPU,编译器,D8 & R8 - 图12

有关 R8 的更多信息,可以参考