kotlin反射_使用R8的Kotlin反射缩小Kotlin库和应用程序

本文翻译自Medium,探讨如何在Kotlin项目中利用R8工具有效地缩小库和应用程序的大小,特别是针对涉及反射的部分。
摘要由CSDN通过智能技术生成

kotlin反射

Co-authored by Morten Krogh-Jespersen and Mads Ager

由Morten Krogh-Jespersen和Mads Ager合着

R8 is the default application shrinker for Android. R8 reduces the size of Android applications by removing unused code and optimizing the code that remains. R8 also has support for shrinking Android libraries. In addition to producing smaller libraries, library shrinking can also be useful to hide new features of your library until you are ready to release them or talk about them publicly.

R8是Android的默认应用程序收缩器。 R8通过删除未使用的代码并优化剩余的代码来减小Android应用程序的大小。 R8还支持缩小Android库。 除了生成较小的库以外,在您准备发布它们或公开讨论它们之前,缩小库还可以用于隐藏库的新功能。

Kotlin is a great language for writing Android applications and libraries. However, shrinking Kotlin libraries or applications that use Kotlin reflection comes with some challenges. Kotlin uses metadata in Java class files to identify Kotlin language constructs. If your application shrinker does not maintain and update Kotlin metadata, your library or application will not work.

Kotlin是编写Android应用程序和库的绝佳语言。 但是,缩小Kotlin库或使用Kotlin反射的应用程序会带来一些挑战。 Kotlin使用Java类文件中的元数据来标识Kotlin语言构造。 如果您的应用程序收缩器无法维护和更新Kotlin元数据,则您的库或应用程序将无法运行。

R8 now has support for maintaining and rewriting Kotlin metadata to fully support shrinking of Kotlin libraries and applications using Kotlin reflection. The support is available in the Android Gradle Plugin version 4.1.0-beta03. Please try it out and let us know how it works for you and file any issues that you encounter in our public bug tracker.

R8现在支持维护和重写Kotlin元数据,以完全支持使用Kotlin反射缩小Kotlin库和应用程序。 Android Gradle插件版本4.1.0-beta03中提供了该支持。 请尝试一下,让我们知道它如何为您工作,并在我们的公共错误跟踪器中记录您遇到的任何问题。

The rest of this blog post provides information on Kotlin metadata and on R8’s support for Kotlin metadata rewriting.

本博文的其余部分提供有关Kotlin元数据以及R8对Kotlin元数据重写的支持的信息。

Kotlin元数据 (Kotlin metadata)

Kotlin metadata is extra information stored in annotations in Java class files produced by the Kotlin JVM compiler. This metadata specifies which Kotlin language construct a given class or method in the class file corresponds to. For example, Kotlin metadata is what allows the Kotlin compiler to recognize that a method in a class file is actually a Kotlin extension function.

Kotlin元数据是由Kotlin JVM编译器生成的Java类文件中的注释中存储的额外信息。 该元数据指定类文件中给定的类或方法所对应的Kotlin语言构造。 例如,Kotlin元数据可以使Kotlin编译器识别出类文件中的方法实际上是Kotlin扩展函数

Let’s have a look at a simple example. The following library code defines a hypothetical base command builder for building compiler commands.

让我们看一个简单的例子。 以下库代码定义了用于构建编译器命令的基本命令构建器。

package com.example.mylibrary


/** CommandBuilderBase contains options common for D8 and 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
}

We can then define a concrete, hypothetical D8CommandBuilder on top of CommandBuilderBase for building a simplified D8 command.

然后,我们可以在CommandBuilderBase顶部定义一个具体的假设D8CommandBuilder ,以构建简化的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
}

The example uses extension functions in order to make sure that if you call the setMinApi method on an D8CommandBuilder the type of the object returned will be D8CommandBuilder and not CommandBuilderBase. These extension functions are top level and they live on the file class CommandBuilderKt for our example. Let’s have a look at that class file using (simplified) javap output.

该示例使用扩展功能,以确保如果调用setMinApi方法上D8CommandBuilder返回将是对象的类型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);
...
}

The javap output shows that the extension functions compile to static methods that take an extra first parameter which is the extension receiver. This information is not enough for the Kotlin compiler to understand that these methods can be used from Kotlin code as extension functions. Therefore, the Kotlin compiler also puts a kotlin.Metadata annotation in the class file. This annotation contains metadata with extra Kotlin-specific information for this class. The annotation shows up if we use the verbose flag withjavap.

javap输出显示扩展功能可以编译为静态方法,这些方法带有一个额外的第一个参数,即扩展接收器。 这些信息不足以让Kotlin编译器理解可以将这些方法用作Kotlin代码的扩展功能。 因此,Kotlin编译器还会在类文件中放置kotlin.Metadata批注 。 此批注包含具有此类的额外Kotlin特定信息的元数据。 如果将冗长标志与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", ...])

The d1 field of the metadata annotation contains most of the actual metadata in the form of a protocol buffer message. The precise contents of the metadata is not important. The important thing is that the Kotlin compiler reads this metadata and uses it to figure out that these methods are extension functions, as illustrated by the following kotlinp dump.

元数据注释的d1字段包含协议缓冲区消息形式的大多数实际元数据。 元数据的准确内容并不重要。 重要的是Kotlin编译器会读取此元数据,并使用它来确定这些方法是扩展功能,如以下kotlinp转储所示。

$ 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...
}

This metadata allows the use of these functions as Kotlin extension functions in Kotlin user code:

此元数据允许将以下功能用作Kotlin用户代码中的Kotlin扩展功能:

D8CommandBuilder().setMinApi(12).setIntermediate(true).build()

R8如何破坏Kotlin库 (How R8 used to break Kotlin libraries)

As the previous section shows, the Kotlin metadata for the class files in a library is really important to be able to use the Kotlin API of the library. However, the metadata is in an annotation and encoded as a protocol buffer message that R8 used to know nothing about. Therefore, R8 used to do one of two things:

如上一节所示,库中类文件的Kotlin元数据对于能够使用库的Kotlin API确实很重要。 但是,元数据在注释中,并被编码为协议缓冲消息,R8对此一无所知。 因此,R8曾经做以下两件事之一:

  1. Throw away the metadata.

    丢弃元数据。
  2. Keep the original metadata.

    保留原始元数据。

Both of these options are bad.

这两个选项都是不好的。

If the metadata is thrown away, the Kotlin compiler will no longer understand that extension functions are extension functions. Therefore, for our example, when compiling code such as D8CommandBuilder().setMinApi(12) the compiler will produce an error stating that no such method exists. That makes sense, because without the metadata, the only thing the Kotlin compiler can see is a Java static method with two parameters.

如果元数据被丢弃,则Kotlin编译器将不再理解扩展功能就是扩展功能。 因此,对于我们的示例,当编译诸如D8CommandBuilder().setMinApi(12)之类的代码时,编译器将产生错误,指出不存在这种方法。 这是有道理的,因为如果没有元数据,Kotlin编译器只能看到的是带有两个参数的Java静态方法。

Keeping the original metadata is bad as well. One of the things that is recorded in Kotlin metadata is super types for classes. So, suppose we only want the D8CommandBuilder class to keep their names when shrinking the library. That would mean that CommandBuilderBase would be renamed, most likely to a. If we leave the original Kotlin metadata, the Kotlin compiler will look for the super type of D8CommandBuilder recorded in the Kotlin metadata. If we use the original metadata, the recorded supertype is CommandBuilderBase and not a. The compilation will therefore fail with an error stating that the super type CommandBuilderBase does not exist.

保留原始元数据也很糟糕。 Kotlin元数据中记录的一件事是类的超类型。 因此,假设我们只希望D8CommandBuilder类在收缩库时保留其名称。 这意味着CommandBuilderBase将被重命名,最有可能被重命名为a。 如果我们保留原始的Kotlin元数据,则Kotlin编译器将查找记录在Kotlin元数据中的D8CommandBuilder的超类型。 如果我们使用原始元数据,则记录的超类型为CommandBuilderBase而不是a. 因此,编译将失败,并显示一条错误,指出超级类型CommandBuilderBase不存在。

R8 Kotlin元数据重写 (R8 Kotlin metadata rewriting)

In order to fix these issues, R8 has been extended with the capability to maintain and rewrite Kotlin metadata. This is done by embedding the Kotlin metadata library developed by JetBrains in R8. The metadata library is used to read the Kotlin metadata in the original input. The metadata information is recorded in R8’s internal data structures. When R8 is done optimizing and shrinking the library or application, it synthesizes new correct Kotlin metadata for all of the Kotlin classes that are explicitly kept.

为了解决这些问题,R8已扩展为具有维护和重写Kotlin元数据的功能。 这是通过将JetBrains开发的Kotlin元数据库嵌入R8中来完成的。 元数据库用于读取原始输入中的Kotlin元数据。 元数据信息记录在R8的内部数据结构中。 当R8完成优化和收缩库或应用程序后,它将为所有明确保留的Kotlin类合成新的正确Kotlin元数据。

Let’s have a look at what that looks like for our example. We put the example code into an Android Studio library project. We enable shrinking by setting minifyEnabled to true as usual in the gradle build file. We update the shrinker configuration to contain the following.

让我们看一下示例的外观。 我们将示例代码放入了Android Studio库项目中。 我们使设置萎缩minifyEnabled为true 在gradle这个build文件如常 。 我们更新收缩器配置以包含以下内容。

# Keep the D8CommandBuilder and all their methods.
-keep class com.example.mylibrary.D8CommandBuilder {
  <methods>;
}


# Keep the extension functions.
-keep class com.example.mylibrary.CommandBuilderKt {
  <methods>;
}


# Keep kotlin.Metadata annotations to maintain metadata on kept items.
-keepattributes RuntimeVisibleAnnotations
-keep class kotlin.Metadata { *; }

This tells R8 to keep D8CommandBuilder and all of the extension functions on CommandBuilderKt. It also tells R8 to keep annotations and in particular to keep the kotlin.Metadata annotation. These rules will only keep Kotlin metadata on the classes that are explicitly kept. Therefore, this will maintain the metadata on D8CommandBuilder and CommandBuilderKt, but not on CommandBuilderBase. We are doing it this way to make sure that you are not shipping a lot of useless metadata in your applications and libraries.

这告诉R8将D8CommandBuilder和所有扩展功能保留在CommandBuilderKt 。 它还告诉R8保留注释,尤其是保留kotlin.Metadata注释。 这些规则只会将Kotlin元数据保留在明确保留的类上。 因此,这将在D8CommandBuilderCommandBuilderKt上保留元数据,但不在 CommandBuilderBase上保留元数据。 我们这样做是为了确保您不会在应用程序和库中提供大量无用的元数据。

Now, building the library with shrinking enabled produces a library in which CommandBuilderBase has been renamed to a. Additionally, the Kotlin metadata for all the kept classes has been rewritten so that any reference to CommandBuilderBase now refers to a. This makes the library work as a Kotlin library as expected.

现在,建设有缩水启用库产生其中一个库CommandBuilderBase已重命名为a 。 此外,所有保留类的Kotlin元数据已被重写,因此对CommandBuilderBase任何引用现在都引用a 。 这使得该库可以像预期的那样作为Kotlin库工作。

As a final note, not keeping the Kotlin metadata on CommandBuilderBase means that the Kotlin compiler will treat the resulting class in the output as a Java class. That can lead to strange completions in your library for Java implementation details of the Kotlin class. To avoid that it is possible to keep the class. If we do, metadata will be maintained. We can use the allowobfuscation modifier on the keep rule to allow R8 to rename the class, while also generating Kotlin metadata for it so the Kotlin compiler and Android Studio will see the class as a Kotlin class.

最后一点,如果不将Kotlin元数据保留在CommandBuilderBase意味着Kotlin编译器会将输出中的结果类视为Java类。 这可能会在您的库中导致Kotlin类的Java实现详细信息的奇怪完成。 为了避免这种情况,可以保留课程。 如果这样做,将保留元数据。 我们可以在keep规则上使用allowobfuscation修饰符,以允许R8重命名该类,同时还为其生成Kotlin元数据,因此Kotlin编译器和Android Studio将该类视为Kotlin类。

-keep,allowobfuscation class com.example.mylibrary.CommandBuilderBase

So far, we have been talking about library shrinking and how Kotlin metadata is needed for Kotlin libraries. Kotlin metadata is also needed for applications that use Kotlin reflection via the kotlin-reflect library. The issues are exactly the same as for libraries. If Kotlin metadata is removed or is not updated correctly, the kotlin-reflect library will not be able to understand the code as Kotlin code.

到目前为止,我们一直在讨论库缩减以及Kotlin库如何需要Kotlin元数据。 对于通过kotlin-reflect库使用Kotlin反射的应用程序,还需要Kotlin元数据。 问题与库完全相同。 如果Kotlin元数据已删除或未正确更新,则kotlin-reflect库将无法将代码理解为Kotlin代码。

As a simple example, say we want to look up and call an extension function on a class at runtime. We want to allow the method to be renamed as we do not care about the name, we just want to find it and call it at runtime.

举一个简单的例子,假设我们要在运行时查找并调用类的扩展函数。 我们希望允许重命名该方法,因为我们不在乎名称,我们只想找到它并在运行时调用它。

class ReflectOnMe() {
    fun String.extension(): String {
        return capitalize()
    }
}


fun reflect(receiver: ReflectOnMe): String {
    return ReflectOnMe::class
        .declaredMemberExtensionFunctions
        .first()
        .call(receiver, "reflection") as String
}

In our code, we add a call: reflect(ReflectOnMe()). This will find the extension function defined on ReflectOnMe and call it using the given ReflectOnMe instance as the receiver and the string “reflection” as the extension receiver.

在我们的代码中,我们添加了一个调用: reflect(ReflectOnMe()) 。 这将找到在ReflectOnMe定义的扩展函数,并使用给定的ReflectOnMe实例作为接收器,并使用字符串“reflection”作为扩展接收器来调用它。

Now that R8 correctly rewrites Kotlin metadata on all kept classes, we can make this work when shrinking the app using the following shrinker configuration.

现在,R8可以在所有保留的类上正确重写Kotlin元数据,我们可以在使用以下收缩器配置收缩应用程序时使此工作正常进行。

# Keep the class that is reflected on and its methods.
-keep,allowobfuscation class ReflectOnMe {
  <methods>;
}


# Keep kotlin.Metadata annotations to maintain metadata on kept items.
-keepattributes RuntimeVisibleAnnotations
-keep class kotlin.Metadata { *; }

This allows both ReflectOnMe and extension to be renamed, it maintains and rewrites the Kotlin Metadata and the app works.

这样就可以重命名ReflectOnMeextension ,并维护和重写Kotlin元数据,并且该应用程序可以正常工作。

试试看! (Give it a try!)

Please try R8’s support for Kotlin metadata rewriting on Kotlin library projects as well as on Kotlin projects using Kotlin reflection. The support is available in the Android Gradle Plugin starting in version 4.1.0-beta03. If you encounter any issues using it, please file bug reports in our public bug tracker.

请在Kotlin库项目以及使用Kotlin反射的Kotlin项目上尝试R8对Kotlin元数据重写的支持。 从版本4.1.0-beta03开始,Android Gradle插件中提供了该支持。 如果您在使用它时遇到任何问题,请在我们的公共错误跟踪器中提交错误报告。

Happy shrinking!

快乐缩水!

翻译自: https://medium.com/androiddevelopers/shrinking-kotlin-libraries-and-applications-using-kotlin-reflection-with-r8-6fe0a0e2d115

kotlin反射

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值