R8 编译器: 为 Kotlin 库和应用 "瘦身"
Java 类文件中的元数据
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-metadata/
Issue Tracker 页面
https://issuetracker.google.com/issues/new?component=326788&template=1025938
Kotlin 元数据
Kotlin 元数据
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-metadata/
Kotlin 扩展函数
https://kotlinlang.org/docs/reference/extensions.html
我们来看一个简单的例子,以下库代码定义了一个假想的用于指令构建的基类,用于构建编译器指令。
package com.example.mylibrary
/** CommandBuilderBase 包含 D8 和 R8 中通用的选项 */
abstract class CommandBuilderBase {
internal var minApi: Int = 0
internal var inputs: MutableList<String> = mutableListOf()
abstract fun getCommandName(): String
abstract fun getExtraArgs(): String
fun build(): String {
val inputArgs = inputs.joinToString(separator = " ")
return "${getCommandName()} --min-api=$minApi $inputArgs ${getExtraArgs()}"
}
}
fun <T : CommandBuilderBase> T.setMinApi(api: Int): T {
minApi = api
return this
}
fun <T : CommandBuilderBase> T.addInput(input: String): T {
inputs.add(input)
return this
}
然后,我们可以定义一个假想 D8CommandBuilder 的具体实现,它继承自 CommandBuilderBase,用于构建简化的 D8 指令。
package com.example.mylibrary
/** D8CommandBuilder to build a D8 command. */
class D8CommandBuilder: CommandBuilderBase() {
internal var intermediateOutput: Boolean = false
override fun getCommandName() = "d8"
override fun getExtraArgs() = "--intermediate=$intermediateOutput"
}
fun D8CommandBuilder.setIntermediateOutput(intermediate: Boolean) : D8CommandBuilder {
intermediateOutput = intermediate
return this
}
$ javap com/example/mylibrary/CommandBuilderKt.class
Compiled from "CommandBuilder.kt"
public final class CommandBuilderKt {
public static final <T extends CommandBuilderBase> T addInput(T, String);
public static final <T extends CommandBuilderBase> T setMinApi(T, int);
...
}
从 javap 的输出内容里可以看到扩展函数被编译为静态方法,该静态方法的第一个参数是扩展接收器。不过这些信息还不足以告诉 Kotlin 编译器这些方法需要作为扩展函数在 Kotlin 代码中调用。所以,Kotlin 编译器还在类文件中增加了 kotlin.Metadata 注解。注解中的元数据里包含本类中针对 Kotlin 特有的信息。如果我们使用 verbose 选项就可以在 javap 的输出中看到这些注解。
$ javap -v com/example/mylibrary/CommandBuilderKt.class
...
RuntimeVisibleAnnotations:
0: kotlin/Metadata(
mv=[...],
bv=[...],
k=...,
xi=...,
d1=["^@.\n^B^H^B\n^B^X^B\n^@\n^B^P^N\n^B...^D"],
d2=["setMinApi", ...])
kotlin.Metadata 注解
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-metadata/
$ kotlinp com/example/mylibrary/CommandBuilderKt.class
package {
// signature: addInput(CommandBuilderBase,String)CommandBuilderBase
public final fun <T : CommandBuilderBase> T.addInput(input: kotlin/String): T
// signature: setMinApi(CommandBuilderBase,I)CommandBuilderBase
public final fun <T : CommandBuilderBase> T.setMinApi(api: kotlin/Int): T
...
}
该元数据表明这些函数将在 Kotlin 用户代码中作为 Kotlin 扩展函数使用:
D8CommandBuilder().setMinApi(12).setIntermediate(true).build()
R8 过去是如何破坏 Kotlin 开发库的
正如前文所提到的,为了能够在库中使用 Kotlin API,Kotlin 的元数据非常重要,然而,元数据存在于注解中,并且会以 protocol buffer 消息的形式存在,而 R8 是无法识别这些的。因此,R8 会从下面两个选项中择其一:
去除元数据
保留原始的元数据
R8 重写 Kotlin 元数据
Kotlin 元数据开发库
https://github.com/JetBrains/kotlin/blob/master/libraries/kotlinx-metadata/jvm/ReadMe.md
#保留 D8CommandBuilder 和它的全部方法
-keep class com.example.mylibrary.D8CommandBuilder {
<methods>;
}
#保留扩展函数
-keep class com.example.mylibrary.CommandBuilderKt {
<methods>;
}
#保留 kotlin.Metadata 注解从而在保留项目上维持元数据
-keepattributes RuntimeVisibleAnnotations
-keep class kotlin.Metadata { *; }
gradle.build 文件
https://developer.android.google.cn/studio/build/shrink-code#enable
上述内容告诉 R8 保留 D8CommandBuilder 以及 CommandBuilderKt 中的全部扩展函数。它还告诉 R8 保留注解,尤其是 kotlin.Metadata 注解。这些规则仅仅适用于那些被显式声明保留的类。因此,只有 D8CommandBuilder 和 CommandBuilderKt 的元数据会被保留。但是 CommandBuilderBase 中的元数据不会被保留。我们这么处理可以减少应用和开发库中不必要的元数据。
现在,启用缩减后所生成的库,里面的 CommandBuilderBase 被重命名为 a。此外,所保留的类的 Kotlin 元数据也被重写,这样所有对于 CommandBuilderBase 的引用都被替换为对 a 的引用。这样开发库就可以正常使用了。
-keep,allowobfuscation class com.example.mylibrary.CommandBuilderBase
到这里,我们介绍了库缩减和 Kotlin 元数据对于 Kotlin 开发库的作用。通过 kotlin-reflect 库使用 Kotlin 反射的应用同样需要 Kotlin 元数据。应用和开发库所面临的问题是一样的。如果 Kotlin 元数据被删除或者没有被正确更新,kotlin-reflect 库就无法将代码作为 Kotlin 代码进行处理。
举个简单的例子,比如我们希望在运行时查找并且调用某个类中的一个扩展函数。我们希望启用方法重命名,因为我们并不关心函数名,只要能在运行时找到它并且调用即可。
class ReflectOnMe() {
fun String.extension(): String {
return capitalize()
}
}
fun reflect(receiver: ReflectOnMe): String {
return ReflectOnMe::class
.declaredMemberExtensionFunctions
.first()
.call(receiver, "reflection") as String
}
在代码中,我们添加了一个调用: reflect(ReflectOnMe())。它会找到定义在 ReflectOnMe 中的扩展函数,并且使用传入的 ReflectOnMe 实例作为接收器,"reflection" 作为扩展接收器来调用它。
现在 R8 可以在所有保留类中正确重写 Kotlin 元数据,我们可以通过使用下面的缩减器配置启用重写。
#保留反射的类和它的方法
-keep,allowobfuscation class ReflectOnMe {
<methods>;
}
#保留 kotlin.Metadata 注解从而在保留项目上维持元数据
-keepattributes RuntimeVisibleAnnotations
-keep class kotlin.Metadata { *; }
尝试一下吧!
Issue Tracker 页面 https://issuetracker.google.com/issues/new?component=326788&template=1025938
推荐阅读