作者 / Google 软件工程师 SørenGjesse 和 Christoffer Adamsen
人们更偏向于装置并保留较小和装置占用空间更小的利用,在新兴市场中尤为显著。有了 R8 编译器,您能够通过压缩、混同和优化,更全面的放大利用体积。
本文咱们将对 R8 的个性进行一个简要的介绍,并介绍可预期的代码缩减水平以及如何在 R8 中启用这些性能。
R8 的压缩个性
R8 通过上面 4 项个性来缩小 Android 利用大小:
- 摇树优化 (Tree shaking): 应用动态代码剖析来查找和删除无法访问的代码和未实例化的类型;
- 优化 : 通过删除有效代码,选择性内联,移除未应用的参数和类合并来优化代码大小;
- 重命名标识,即混同解决 : 应用短名称以及缩短包命名空间;
- 缩小调试信息 : 规范化调试信息并压缩行号信息。
为什么须要 R8 压缩
开发利用时,所有代码都应有目标并在利用中实现相应性能。不过,大多数利用都会应用 Jetpack、OkHttp、Guava、Gson 和 Google Play 服务 等第三方库,并且用 Kotlin 编写的利用始终蕴含 Kotlin 规范库。当您应用这其中的某个第三方库时,您的利用中通常只应用其中很小一部分。若不压缩,所有库代码都会保留在您的利用中。
您的代码大小也可能比理论须要的大,因为简短的代码有时能够进步可读性和可维护性: 例如,您可能会尽量应用有意义的变量名和建造者模式 (builder pattern) 来帮忙其他人更容易检查和了解您的代码。然而这些模式会加大代码量。通常,您本人编写的代码有很大的压缩空间。
启用 R8 来压缩您的利用
要在 release build 上启用 R8 压缩,须要在利用的主 build.gradle 文件中将 minifyEnable 属性设置为 true,如下所示:
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 示例利用)。
根本压缩算法
为简略起见,咱们写了一个基于 Java 编程语言的程序作为参考:
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();
}
}
程序的入口是 static void main 办法,咱们应用以下 keep 规定 指定该办法:
-keep class com.example.JavaHelloWorld {public static void main(java.lang.String[]);
}
R8 缩减算法的运作形式如下:
- 首先,它从程序常见的入口点跟踪所有可拜访的代码。这些入口点由 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:
-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 压缩器如何运作吗?请参考 R8 开发者文档 理解更多!