- 英文版原文
- 中文版原文
- 使用 R8 压缩您的应用
CPU & JVM
在最开始的时候,CPU 只能处理简单的数学运算,比如加减乘除。经过这么多年的发展,CPU 已经进化到可以处理非常复杂的运算,比如图片处理、音频解码等等。目前最知名的移动处理器是高通生产的骁龙系列。
但高通并不是唯一的 CPU 制造商。其他制造商生产的 CPU 有些架构与高通相同,有些却不一样。这里我要说:“欢迎来到地狱!”。如果你曾经开发过 C++/C,你就会知道 native code 需要为所有支持的架构编译一份,比如 ARM、ARM64、X86、X64、MIPS 等等。
作为一个 Android 开发者,通常你的应用需要支持多种多样的设备,而这些设备背后的 CPU 架构不尽相同。这基本上意味着你需要为每一种架构编译一个 so 文件。
JVM 完美解决了这个问题。JVM 在硬件上面添加了一层抽象。通过这种方式,你的应用程序就可以 通过 Java 的接口来使用 CPU ,而你也不用去为了不同的 CPU 架构做适配,也不用为了 Mac 上与众不同的蓝牙驱动而烦恼。
javac 编译器将你的 Java 代码编译为字节码(.class 文件),然后你的代码就可以直接在 Java 虚拟机上运行,而不用关心底层操作系统的差异。作为一个应用开发者,你不用去关心设备硬件、操作系统、内存、CPU 的差异,只需要关注业务逻辑。
JVM 内部
JVM 有三个主要的区域:
- ClassLoader - 主要职责是加载编译后的字节码(.class 文件),链接,检测损坏的字节码,定位并初始化静态变量和静态代码
- Runtime Data - 负责所有的程序数据:栈,方法变量,当然还有我们都非常熟悉的堆
- Execution Engine - 负责执行已经加载的代码并清理不在需要的垃圾(GC)
Interpreter & JIT
这两个家伙在一起工作,每当我们运行我们的程序,解释器都需要将字节码解释为 机器码 再运行。这么做最主要的一个缺点就是当一个方法需要多次执行的时候,每次执行都需要进行解释。
JIT 编译器就是用来解决这个问题的。执行引擎还是使用解释器解析代码,但不同的是,当它发现有重复执行的代码时,它会切换为 JIT 编译器,JIT 编译器会将这些重复的代码编译为 本地机器代码,而当同样的方法再次被调用时,已经被编译好的本地机器代码就会被直接运行,从而提升系统的性能。这些重复执行的代码也被称为「热代码(Hot code)」。
Dalvik
Java 虚拟机的设计一直以来都是面向有无限电量和几乎无限存储的设备。
而 Android 设备则很不相同。首先电池容量有限,所有的程序都需要为了有限的资源竞争。其次内存的大小有限,存储空间也很有限(跟其他的 JVM 运行设备相比,简直是小的可怜)。因此,当 Google 决定在移动设备上使用 JVM 的时候,他们做了很多的改动 - 包括 java 代码编译为字节码的过程以及字节码的结构等等。
之所以有这样的区别是因为普通的 Java 字节码是以栈为基础的(所有的变量都存储在栈中),而 dex 格式的字节码是是 寄存器为基础的(所有的变量都存储在寄存器中)。后者更加高效并且需要更少的空间。运行 Dex 字节码的 Android 虚拟机被称为 Dalvik。
Android 构建系统
ART
Dalvik 曾经是一个很不错的解决方案,然而它也有不少局限性。所以呢,google 后来又推出了一个优化后的 Java 虚拟机,叫 ART。ART 与 Dalvik 的主要区别是,它不是在运行时进行解释和 JIT 编译,而是直接运行的提前编译好的 .oat 文件,因此获得了更好更快的运行速度。为了提前编译好 .oat 二进制文件,ART 使用了 AOT 编译器(AOT 是 Ahead of Time 的缩写)
那么,到底什么是 .oat 二进制文件呢?
当你从应用商店下载并安装一个应用的时候,除了解压缩 .apk 文件,系统也会对 .dex 文件进行编译,生成 .oat 文件。
所以当你点击应用图标的时候,ART 直接加载 .oat 文件并运行,而不需要任何的解释和 JIT 步骤。
- 这会导致安装或更新应用的速度特别慢
- 所有的 .dex 文件都会被编译为 .oat 文件,即使有些代码几乎不被用户使用
Google 的工程师想出了一个绝妙的点子来解决问题,充分利用了 Interpreter/JIT/AOT 的优点:
- 最开始安装的时候并没有 .oat 文件生成,当你第一次运行应用的时候,ART 会使用解释器来解释执行 .dex 代码
- 当 Hot Code 被发现的时候,ART 会调用 JIT 来对代码进行编译
- 使用 JIT 编译过的代码以及编译选项会存储在缓存中,以后每次执行同样的代码就会使用这里的缓存
- 当设备空闲的时候(屏幕熄灭并且在充电),所有的 Hot Code 会被 AOT 编译器使用缓存的编译选项编译为 .oat 文件
- 当你再次运行应用的时候,位于 .oat 文件的代码会被直接执行,从而获得更好的性能,而如果要执行的代码不在 .oat 文件中,则回到第一步
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 字节码。这不可避免会导致一个问题 - 更长的编译时间。
Dope8
为了解决这个问题,在 Android Studio 3.2 中,Google 使用 D8 替换了旧的 dx 编译器。D8 的主要改进是消除 desuguaring 的过程,让其成为 dex 编译的一部分,从而加快编译速度。
R8 替代 Proguard
Proguard 也是编译过程中一个 transformation 的步骤,当然也会影响编译时间。为了解决这个问题,R8 在 dex 过程中也会做类似的事情(比如优化、混淆、清理无用的类),而避免了多一步 transformation。
有关 R8 的更多信息,可以参考