使用 R8 压缩您的应用
R8 的压缩特性
R8 通过下面 4 项特性来减少 Android 应用大小:
摇树优化 (Tree shaking): 使用静态代码分析来查找和删除无法访问的代码和未实例化的类型;
优化: 通过删除无效代码,选择性内联,移除未使用的参数和类合并来优化代码大小;
重命名标识,即混淆处理: 使用短名称以及缩短包命名空间;
减少调试信息: 规范化调试信息并压缩行号信息。
为什么需要 R8 压缩
开发应用时,所有代码都应有目的并在应用中实现相应功能。不过,大多数应用都会使用 Jetpack、OkHttp、Guava、Gson 和 Google Play 服务等第三方库,并且用 Kotlin 编写的应用始终包含 Kotlin 标准库。当您使用这其中的某个第三方库时,您的应用中通常只使用其中很小一部分。若不压缩,所有库代码都会保留在您的应用中。
Jetpack https://developer.android.google.cn/jetpack OkHttp https://square.github.io/okhttp/
Guava https://github.com/google/guava Gson https://github.com/google/gson
Google Play 服务 https://developers.google.cn/android/guides/overview Kotlin 标准库 https://kotlinlang.org/api/latest/jvm/stdlib/
启用 R8 来压缩您的应用
android {
buildTypes {
release {
minifyEnabled true
}
}
}
别被 minifyEnable 这个名字所迷惑,它会启用 R8 的代码缩减功能。
R8 能缩减多少应用大小?
R8 可以大大减小应用的大小。例如,去年的 Google I/O 应用大小为 18.55 MB,压缩前包含 150,220 个方法和 3 个 DEX 文件。压缩后,应用大小缩小到 6.45 MB,包含 45,831 个方法和 1 个 DEX 文件。R8 缩减了 65% 的 DEX 文件大小 (测量数据来自 Android Studio 3.5.1 和 IOSched 示例应用)。
IOSched 示例应用 https://github.com/google/iosched
基本压缩算法
class com.example.JavaHelloWorld {
private void unused() {
System.out.println("Unused");
}
private static void greeting() {
System.out.println("Hello, world!");
}
public static void main(String[] args) {
greeting();
}
}
keep 规则 https://developer.android.google.cn/studio/build/shrink-code#configuration-files
-keep class com.example.JavaHelloWorld {
public static void main(java.lang.String[]);
}
首先,它从程序常见的入口点跟踪所有可访问的代码。这些入口点由 R8 keep 规则定义。例如,在此 Java 代码示例中,R8 会在 main 方法处开始运行。
在该示例中,R8 从 main 方法跟踪到 greeting 方法。greeting 方法是在运行时被调用的,因此跟踪在此处停止。
跟踪完成后,R8 使用摇树优化来删除未使用的代码。在此示例中,摇树删除了未使用的方法,因为 R8 的跟踪过程检测到从任何已知的入口都无法到达该方法。
接下来,R8 将标识重命名为较短的名称,这些名称在 DEX 文件中占用较少的空间。在示例中,R8 可能会将 greeting 方法重命名为短名称 a:
class com.example.JavaHelloWorld {
private static void a() {
System.out.println("Hello, world!");
}
public static void main(String[] args) {
a();
}
}
最后,应用代码优化。缩减代码大小的内联是其一。在此示例中,将方法 a 的主体直接迁移到 main 中,代码会显得更简洁:
class com.example.JavaHelloWorld {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
如您所见,处理后的代码比原始代码短得多。
使用 R8 压缩应用前的准备工作
正如独立的 Java 程序一样,Android 应用有许多常见的入口点: Activity (活动),Service (服务),Content Provider (内容提供者) 和 Broadcast Receiver (广播接收者)。aapt2 工具通过基于 Android Manifest 文件生成 keep 规则来为您处理这些入口点。
除了这些熟知的入口点,Android 应用还需要其他标准的 keep 规则。这些规则由 Android Gradle 插件提供,您可以在配置构建时指定该默认配置文件:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
}
}
}
应用代码中的反射
反射 (Reflection) 会导致 R8 在跟踪代码时无法识别到代码的入口点。第三方库也可能用到反射,并且由于第三方库实际上是您的应用的一部分,您 (作为应用开发者) 将负责这些库以及您自己的代码中使用的反射。第三方库可能附带了它们自己的规则,但是切记,有些库不一定是为 Android 编写的,抑或是未考虑缩减问题,因此它们可能需要其他配置。
以一个 Kotlin 类为例,该类具有一个名为 name 的字段和一个 main 方法,该方法创建一个实例并将该实例序列化为 JSON:
class Person(val name: String)
fun printJson() {
val gson = Gson()
val person = Person("Søren Gjesse")
println(gson.toJson(person))
}
缩减代码后,运行程序将输出一个空的 JSON 对象 {}。这是因为 R8 仅将字段名视为写入 (在 Person 构造函数中),但从未读取,因此 R8 会将其移除。最后 Person 丢失了字段值,造成空的 JSON 对象。但是,该字段由 Gson 序列化读取,而 Gson 使用反射的方式来执行此操作,因此 R8 无法看到此字段已被读取。
要保留名称字段,请在您的 proguard-rules.pro 文件中添加一个保留规则 -keep:
proguard-rules.pro
https://developer.android.google.cn/studio/build/shrink-code#add-configuration
-keep class com.example.myapplication.Person {
public java.lang.String name;
}
此规则告诉 R8 不要处理 Person 类中的 name 的字段。将其放置在适当位置后,运行代码即可得到预期的 JSON 对象 {"name": "SørenGjesse"} 。
最后,在配置项目时,请确保将 proguard-rules.pro 文件添加到 build.gradle 配置中:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
}
了解更多
R8 开发者文档
https://developer.android.google.cn/studio/build/shrink-code
推荐阅读