乐趣区

关于kotlin:R8-编译器-为-Kotlin-库和应用-瘦身

作者 / Morten Krogh-Jespeersen, Mads Ager

R8 是 Android 默认的程序缩减器,它能够通过移除未应用的代码和优化其余代码的形式升高 Android 利用大小,R8 同时也反对缩减 Android 库大小。除了生成更小的库文件,库压缩操作还能够暗藏开发库里的新个性,等到这些个性绝对稳固或者能够面向公众的时候再对外开放。

Kotlin 对于编写 Android 利用和开发库来说是十分棒的开发语言。不过,应用 Kotlin 反射来缩减 Kotlin 开发库或者利用就没那么简略了。Kotlin 应用 Java 类文件中的元数据 来辨认 Kotlin 语言中的构造。如果程序缩减器没有保护和更新 Kotlin 的元数据,相应的开发库或者利用就无奈失常工作。

R8 当初反对维持和重写 Kotlin 的元数据,从而全面反对应用 Kotlin 反射来压缩 Kotlin 开发库和利用。该个性实用于 Android Gradle 插件版本 4.1.0-beta03。欢送大家踊跃尝试,并在 Issue Tracker 页面 向咱们反馈整体应用感触和遇到的问题。

本文接下来的内容为大家介绍了 Kotlin 元数据的相干信息以及 R8 中对于重写 Kotlin 元数据的反对。

Kotlin 元数据

Kotlin 元数据 是存储在 Java 类文件的注解中的一些额定信息,它由 Kotlin JVM 编译器生成。元数据确定了类文件中的类和办法是由哪些 Kotlin 代码形成的。比方,Kotlin 元数据能够通知 Kotlin 编译器类文件中的一个办法实际上是 Kotlin 扩大函数。

咱们来看一个简略的例子,以下库代码定义了一个假想的用于指令构建的基类,用于构建编译器指令。

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
}

下面的示例应用的扩大函数来保障当您在 D8CommandBuilder 上调用 setMinApi 办法的时候,所返回的对象类型是 D8CommandBuilder 而不是 CommandBuilderBase。在咱们的示例中,这些扩大函数属于顶层的函数,并且仅存在于 CommandBuilderKt 类文件中。接下来咱们来看一下通过精简后的 javap 命令所输入的内容。

$ 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()

R8 过来是如何毁坏 Kotlin 开发库的

正如前文所提到的,为了可能在库中应用 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 元数据

为了解决上述问题,扩大后的 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 页面中提交问题。

退出移动版