目录
我们都知道 Java 是跨平台的,一次编译到处运行,同一套 Java 代码可以在 Windows、Linux、Mac 上运行,背后依赖于不同平台/版本的 JVM(Java 虚拟机)。Java 代码编译后生成 .class
字节码文件,再由 JVM 翻译成特定平台的机器码,然后运行。
JVM 的组成结构
JVM 有三个主要的部分:
- ClassLoader – 主要职责是加载编译后的字节码(.class 文件),验证链接,检测损坏的字节码,分配和初始化静态变量和静态代码。
- Runtime Data – 负责所有程序数据:堆、栈、方法变量。
- Execution Engine(执行引擎)- 执行已经编译和加载的代码、清理生成的所有垃圾(垃圾收集器)。
Interpreter(解释器) & JIT
- 解释器 – 负责将字节码解释为机器码。
- JIT – 将代码编译为本地机器码。
每次运行程序时,执行引擎都会利用 Interpreter 将字节码解释为机器码。当发现有重复执行的代码时,会切换为JIT编译器。JIT 编译器会将重复的代码编译为本地机器码,当同样的方法被调用时,直接运行本地机器码,从而提高系统性能。
Dalvik 虚拟机
JVM 的设计是面向无限电量/存储的设备,Android 设备与之相比,太弱鸡了 (电量、内存、存储等小的可怜)。
由于在 Android 设备上不能直接使用 JVM 虚拟机,于是 Google 自己设计了一套用于 Android 平台的 Java 虚拟机 —— Dalvik,支持已转换为 .dex
(Dalvik Executable)压缩格式的 Java 应用程序的运行。
常规 Java 字节码是基于堆栈的(所有变量都存储在堆栈中),而 dex 字节码是基于寄存器的(所有变量都存储在寄存器中)。与常规 Java 字节码相比,dex 方法效率更高,需要的空间更少。
Android 打包构建过程
把 APK 安装到设备上,当点击应用图标时,系统会启动一个新的 Dalvik 进程,并将应用包含的 dex 代码加载进来,在运行时交由 Interpreter 或 JIT 编译,然后就可以看到应用的界面了。
特点:在 Dalvik 中,应用的每次运行都需要执行解释操作,而这段时间是计入程序的执行时间,所以程序的启动速度会有点慢,当然也有好处, 应用安装速度快。
ART 虚拟机
在 Android 4.4.4 后,Google 开始引入 Dalvik 的替代品—— ART。
ART 与 Dalvik 的主要区别是,它不是在运行时进行解释和 JIT 编译,而是直接运行的提前编译好的 .oat 文件,因此获得了更好更快的运行速度。ART 使用了 AOT 编译器(AOT 是 Ahead of Time 的缩写)在应用首次安装时将 dex
提前编译为 .oat
二进制文件。
- 安装应用程序时,除了解压 .apk 文件外,同时也会将 . dex文件转换为 . oat 二进制文件。
- 点击应用图标启动时,ART 直接加载. oat 文件并运行,启动速度明显提升,避免了重复编译,减少了 CPU 的使用频率,也降低了功耗,当然缺点也是有的:更长的应用安装时间和更大的存储空间占用。
这个方案感觉还不错,但是还是存在问题:
- 将 .dex 文件转换为 .oat 文件这个过程放到安装应用到过程中,增加了安装或升级应用程序所需的时间。此外,每次 Android 操作系统更新都会启动 1 或 2 小时的“优化应用程序”过程。这是一个巨大的痛苦,尤其是对于每月获得安全更新的 Nexus 用户而言。
- 整个 . dex 文件被编译成 . oat,甚至是用户很少使用或根本不使用的部分(例如,用户设置一次就再也没有返回的应用程序设置,或登录页面)。所以基本上,我们浪费了磁盘空间,这对于存储有限的低端设备来说是一个问题。
基于以上,谷歌工程师提出了一个绝妙的主意,将 解释器、JIT 和 AOT 结合起来:
- 最开始安装的时候并没有 .oat 文件生成,当你第一次运行应用的时候,ART 会使用解释器来解释执行 .dex 代码;
- 当检测到 HOT Code 时,它会使用 JIT 编译器进行编译;
- 使用 JIT 编译过的代码以及编译选项会存储在缓存中,以后每次执行同样的代码就会使用这里的缓存;
- 一旦设备处于空闲状态(屏幕关闭或充电),使用 AOT 编译器和配置文件重新编译 HOT Code;
- 当再次运行应用的时候,位于 .oat 文件的代码会被直接执行,从而获得更好的性能,而如果要执行的代码不在 .oat 文件中,则回到第 1 步。
平均而言,优化 80% 的应用程序大约需要运行 8 次应用程序。
还有更大的优化空间——在类似设备之间共享编译配置文件。
当设备空闲并连接到 Wi-Fi 网络时,它会通过 Google Play 服务共享编译配置文件。稍后,当另一个使用相同设备的用户从 Play 商店下载该应用程序时,该设备会收到这些 Profiles for AOT 以执行引导编译。结果 – 用户从第一次使用时就获得了优化的应用程序。
DX/D8/R8
上面说过,Android 虚拟机采用基于寄存器的指令集(opcodes),这样会存在一个问题,更高版本 Java 新引入的语法特性不能在上面直接使用。
为了让我们能使用上 Java 的新特性,Google 使用 Transformation
来增加了一步编译过程 → 脱糖(desugaring)。其实就是将我们代码里使用的 java 新特性翻译为 Dalvik/ART 能够识别的 java 字节码。
通过上图可以看出,由于增加一个 desugar 步骤,所以不可避免会引入一个新问题 —— 更长的编译时间。
为了解决这个问题,在 Android Studio 3.2 中,谷歌用一个名为 Dope8(简称D8)的新编译器替换了 Dex 编译器。主要思想是 消除脱糖转换 并使其成为 .class2dex 编译的一部分,从而缩短构建时间。
R8 是 D8 的衍生产品。它们共享相同的代码库。但 R8 解决了额外的难题。与 D8 一样,R8 将让我们在旧的 Dalvik/ART 上使用新的 Java 新功能。但不仅如此,R8 带来的最大改进之一是优化了我们的 . dex代码,只留下支持应用程序中某些设备/API 级别所需的操作码。
R8 会在 .class 到 .dex 编译期间做与 Proguard 类似的操作(优化、混淆、删除未使用的类),而不是作为单独的转换。
需要说明的是,R8 不是 Proguard。这是一个新的实验工具,它只支持 Proguard 所做的事情的一个子集。你可以在这里阅读更多关于它的信息。
R8 对 Kotlin 更友好。Kotlin 可以让我们写更优雅易读易维护的代码,但是 Kotlin 生成的字节码指令比对应的 Java 版本要多一些,使用 R8 可以比 使用Proguard 减少一部分指令,从而减少构建时间、软件包体积。
用 ProGuard 还是 R8?
如果没有历史包袱,直接 R8,毕竟兼容绝大部分的 ProGuard 规则,更快的编译速度,对 Kotlin 更友好,支持现有 ProGuard 规则,更快更强,Android Gradle Plugin(AGP) 3.4.0 或更高版本,默认使用 R8 混淆编译器。
如果不想用 R8,想用回 ProGuard 的话 (可以但没必要),可以在 gradle.properties
文件中添加下述配置禁用 R8:
android.enableR8=false
android.enableR8.libraries=false
编译 APK 时可能会报错:
Waring: there were x unresolved references to classes or interfaces.
You may need to add missing library jars or update their versions.
If your code works fine without the missing classes, you can suppress
the warning with '-dontwarn' options.
(http://proguard.sourceforge.net/manual/troubleshooting.html#unresolvedclass)
FAILURE: Build failed with an exception.
在 proguard-rules.pro
文件中加上 -ignorewarnings
即可解决。
另外,使用 ProGuard 或 R8 构建项目会在 build\outputs\mapping\release
输出下述文件:
- mapping.txt → 原始与混淆过的类、方法、字段名称间的转换;
- seeds.txt → 未进行混淆的类与成员;
- usage.txt → APK 中移除的代码;
- resources.txt → 资源优化记录文件,哪些资源引用了其他资源,哪些资源在使用,哪些资源被移除。
Tips:上述文件不一定都有,R8 可以在
proguard-rules.pro
文件添加下述配置输出对应文件:
# 输出mapping.txt文件
-printmapping ./build/outputs/mapping/mapping.txt
# 输出seeds.txt文件
-printseeds ./build/outputs/mapping/seeds.txt
# 输出usage.txt文件
-printusage ./build/outputs/mapping/usage.txt
自定义混淆字典
之前在反编译人家的 APP 时看到标识符竟然不是 abcd,而是中文和特殊字符,怎么做到的呢?其实不难,自定义一个混淆字典就好,在 app 的 proguard-rules
的同级目录创建一个文件,比如 dictionary,内容示例如下:
﹢
﹣
×
÷
...太长省略
接着在 proguard-rules
添加下述配置:
-obfuscationdictionary ./dictionary
-classobfuscationdictionary ./dictionary
-packageobfuscationdictionary ./dictionary
模块化混淆
混淆的开启由 app 模块控制,与子模块无关。
建议在 app 模块设置公共混淆规则,子模块设置专属混淆规则,子模块区分 project 和 aar:
# Project类型,配置方法同app模块
buildTypes {
release {
minifyEnabledfalse
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
# AAR类型
android {
defaultConfig {
consumerProguardFiles'lib-proguard-rules.txt'
}
...
}
当然,你想让混淆规则都由 app 模块控制也是可以的,移除模块时记得删掉对应的混淆就好~