做者 / Morten Krogh-Jespeersen, Mads Agerhtml
R8 是 Android 默认的程序缩减器,它能够经过移除未使用的代码和优化其他代码的方式下降 Android 应用大小,R8 同时也支持缩减 Android 库大小。除了生成更小的库文件,库压缩操做还能够隐藏开发库里的新特性,等到这些特性相对稳定或者能够面向公众的时候再对外开放。java
Kotlin 对于编写 Android 应用和开发库来讲是很是棒的开发语言。不过,使用 Kotlin 反射来缩减 Kotlin 开发库或者应用就没那么简单了。Kotlin 使用 Java 类文件中的元数据 来识别 Kotlin 语言中的结构。若是程序缩减器没有维护和更新 Kotlin 的元数据,相应的开发库或者应用就没法正常工做。android
R8 如今支持维持和重写 Kotlin 的元数据,从而全面支持使用 Kotlin 反射来压缩 Kotlin 开发库和应用。该特性适用于 Android Gradle 插件版本 4.1.0-beta03。欢迎你们踊跃尝试,并在 Issue Tracker 页面 向咱们反馈总体使用感觉和遇到的问题。git
本文接下来的内容为你们介绍了 Kotlin 元数据的相关信息以及 R8 中对于重写 Kotlin 元数据的支持。github
Kotlin 元数据 是存储在 Java 类文件的注解中的一些额外信息,它由 Kotlin JVM 编译器生成。元数据肯定了类文件中的类和方法是由哪些 Kotlin 代码构成的。好比,Kotlin 元数据能够告诉 Kotlin 编译器类文件中的一个方法其实是 Kotlin 扩展函数。api
咱们来看一个简单的例子,如下库代码定义了一个假想的用于指令构建的基类,用于构建编译器指令。数据结构
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 指令。jvm
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 }
上面的示例使用的扩展函数来保证当您在 D8CommandBuilder
上调用 setMinApi
方法的时候,所返回的对象类型是 D8CommandBuilder
而不是 CommandBuilderBase
。在咱们的示例中,这些扩展函数属于顶层的函数,而且仅存在于 CommandBuilderKt
类文件中。接下来咱们来看一下经过精简后的 javap
命令所输出的内容。ide
$ 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", ...])
元数据注解的 d1 字段包含了大部分实际的内容,它们以 protocol buffer 消息的形式存在。元数据内容的具体意义并不重要。重要的是 Kotlin 编译器会读取其中的内容,而且经过这些内容肯定了这些方法是扩展函数,以下 Kotlinp
dump 输出内容所示。
$ 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()
正如前文所提到的,为了可以在库中使用 Kotlin API,Kotlin 的元数据很是重要,然而,元数据存在于注解中,而且会以 protocol buffer 消息的形式存在,而 R8 是没法识别这些的。所以,R8 会从下面两个选项中择其一:
可是这两个选项都不可取。
若是去除元数据,Kotlin 编译器就再也没法正确识别扩展函数。好比在咱们的例子中,当编译相似 D8CommandBuilder().setMinApi(12)
这样的代码时,编译器就会报错,提示不存在该方法。这彻底说得通,由于没有了元数据,Kotlin 编译器惟一能看到的就是一个包含两个参数的 Java 静态方法。
保留原始的元数据也一样会出问题。首先 Kotlin 元数据中所保留的类是父类的类型。因此,假设在缩减开发库大小的时候,咱们仅但愿 D8CommandBuilder
类可以保留它的名称。这时候也就意味着 CommandBuilderBase
会被重命名,通常会被命名为 a。若是咱们保留原始的 Kotlin 元数据,Kotlin 编译器会在元数据中寻找 D8CommandBuilder
的超类。若是使用原始元数据,其中所记录的超类是 CommandBuilderBase
而不是 a
。此时编译就会报错,而且提示 CommandBuilderBase
类型不存在。
为了解决上述问题,扩展后的 R8 增长了维护和重写 Kotlin 元数据的功能。它内嵌了 JetBrains 在 R8 中开发的 Kotlin 元数据开发库。元数据开发库能够在原始输入中读取 Kotlin 元数据。元数据信息被存储在 R8 的内部数据结构中。当 R8 完成对开发库或者应用的优化和缩小工做后,它会为全部声明被保留的 Kotlin 类合成新的正确元数据。
来一块儿看一下咱们的示例有哪些变化。咱们将示例代码添加到一个 Android Studio 库工程中。在 gradle.build 文件中,经过将 minifyEnbled
置 true 来启用包大小缩减功能,咱们更新缩减器配置,使其包含以下内容:
#保留 D8CommandBuilder 和它的所有方法 -keep class com.example.mylibrary.D8CommandBuilder { <methods>; } #保留扩展函数 -keep class com.example.mylibrary.CommandBuilderKt { <methods>; } #保留 kotlin.Metadata 注解从而在保留项目上维持元数据 -keepattributes RuntimeVisibleAnnotations -keep class kotlin.Metadata { *; }
上述内容告诉 R8 保留 D8CommandBuilder
以及 CommandBuilderKt
中的所有扩展函数。它还告诉 R8 保留注解,尤为是 kotlin.Metadata
注解。这些规则仅仅适用于那些被显式声明保留的类。所以,只有 D8CommandBuilder
和 CommandBuilderKt
的元数据会被保留。可是 CommandBuilderBase
中的元数据不会被保留。咱们这么处理能够减小应用和开发库中没必要要的元数据。
如今,启用缩减后所生成的库,里面的 CommandBuilderBase
被重命名为 a
。此外,所保留的类的 Kotlin 元数据也被重写,这样全部对于 CommandBuilderBase
的引用都被替换为对 a
的引用。这样开发库就能够正常使用了。
最后再说明一下,在 CommandBuilderBase
中不保留 Kotlin 元数据意味着 Kotlin 编译器会将生成的类做为 Java 类进行对待。这会致使库中 Kotlin 类的 Java 实现细节产生奇怪的结果。要避免这样的问题,就须要保留类。若是保留了类,元数据就会被保留。咱们能够在保留规则中使用 allowobfuscation
修饰符来容许 R8 重命名类,生成 Kotlin 元数据,这样 Kotlin 编译器和 Android Studio 都会将该类视为 Kotlin 类。
-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 { *; }
这样的配置使得缩减器在重命名 ReflectOnMe 和扩展函数的同时,仍然维持而且重写 Kotlin 元数据。
欢迎尝试 R8 对于 Kotlin 库项目中 Kotlin 元数据重写的特性,以及在 Kotlin 项目中使用 Kotlin 反射。该特性能够在 Android Gradle Plugin 4.1.0-beta03 及之后的版本中使用。若是在使用过程当中遇到任何问题,请在咱们的 Issue Tracker 页面中提交问题。