当前位置:网站首页>R8 compiler: tailoring kotlin libraries and Applications

R8 compiler: tailoring kotlin libraries and Applications

2020-11-09 17:44:00 Android Developers

author / Morten Krogh-Jespeersen, Mads Ager

R8 yes Android The default program reducer , It can reduce... By removing unused code and optimizing the rest of the code Android Application size ,R8 It also supports downsizing Android Library size . In addition to generating smaller library files , Library compression can also hide new features in the development library , Wait until these features are relatively stable or open to the public .

Kotlin For writing Android Applications and development libraries are great development languages . however , Use Kotlin To reduce by reflection Kotlin Development libraries or applications are not that simple .Kotlin Use  Java Metadata in class files To identify Kotlin Structure in language . If program reducers are not maintained and updated Kotlin Metadata , The corresponding development library or application will not work properly .

R8 Now support maintenance and rewriting Kotlin Metadata , To fully support the use of Kotlin Reflection to compress Kotlin Development libraries and Applications . This feature applies to Android Gradle The plug-in version 4.1.0-beta03. Welcome to try , And in  Issue Tracker page Give us feedback on the overall use experience and problems encountered .

The next part of this article introduces Kotlin Information about metadata and R8 For rewriting Kotlin Metadata support .

Kotlin Metadata

Kotlin Metadata Is stored in the Java Some additional information in the class file , It consists of Kotlin JVM Compiler generation . Which classes and files in the metadata are determined by Kotlin Made up of code . such as ,Kotlin Metadata can tell Kotlin A method in the compiler class file is actually  Kotlin spread function .

Let's take a simple example , The following library code defines a hypothetical base class for instruction building , Used to build compiler instructions .

package com.example.mylibrary

/** CommandBuilderBase  contain  D8  and  R8  General options in  */

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
}

then , We can define an assumption  D8CommandBuilder  The concrete realization of , It is inherited from  CommandBuilderBase, For building simplified D8 Instructions .

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 above example uses the extension function to ensure that when you are in  D8CommandBuilder  On the call  setMinApi  Method time , The returned object type is  D8CommandBuilder  instead of  CommandBuilderBase. In our example , These extension functions belong to the top level functions , And only exists in  CommandBuilderKt  Class file . Now let's take a look at the streamlining of  javap  What the command outputs .

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

from  javap  You can see that the extension functions are compiled into static methods , The first parameter of the static method is the spread receiver . But this information is not enough to tell Kotlin Compiler these methods need to be used as extension functions in Kotlin Call in code . therefore ,Kotlin The compiler has also added  kotlin.Metadata annotation . The metadata in the annotation contains the information for Kotlin Unique information . If we use verbose It's in the options  javap  You can see these annotations in the output of .

$ 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", ...])

Metadata annotated d1 The field contains most of the actual content , They use protocol buffer The form of the message exists . The concrete meaning of metadata content is not important . It is important to Kotlin The compiler will read it , And through these contents, it is confirmed that these methods are extension functions , as follows  Kotlinp dump The output shows .

$ 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

...
}

The metadata indicates that these functions will be in Kotlin In user code as Kotlin Extension functions use :

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

R8 How the past destroyed Kotlin Development Library

As mentioned earlier , In order to be able to use Kotlin API,Kotlin Metadata is very important , However , Metadata exists in annotations , And will use protocol buffer The form of the message exists , and R8 It's impossible to recognize these . therefore ,R8 You will choose one of the following two options :

  • Remove metadata
  • Keep the original metadata

But neither option is desirable .

If you remove metadata ,Kotlin The compiler will no longer be able to correctly identify extension functions . For example, in our case , When compiling something like  D8CommandBuilder().setMinApi(12)  When it comes to code like this , The compiler will report an error , It is suggested that there is no such method . It makes perfect sense , Because there's no metadata ,Kotlin The only thing the compiler can see is a file with two parameters Java Static methods .

Keeping the original metadata is also problematic . First Kotlin The class reserved in the metadata is the type of the parent class . therefore , Suppose you're reducing the size of your development library , We just hope  D8CommandBuilder  Class can retain its name . And that means  CommandBuilderBase  Will be renamed , It's usually called a. If we keep the original Kotlin Metadata ,Kotlin The compiler looks in the metadata for  D8CommandBuilder  Superclass of . If you use raw metadata , The superclass recorded here is  CommandBuilderBase  instead of  a. At this point, the compiler will report an error , And prompt  CommandBuilderBase  Type does not exist .

R8 rewrite Kotlin Metadata

In order to solve the above problems , Extended R8 Added maintenance and rewriting Kotlin The function of metadata . It's embedded JetBrains stay R8 Developed in  Kotlin Metadata development library . The metadata development library can be read from the original input Kotlin Metadata . Metadata information is stored in R8 In the internal data structure of . When R8 After completing the optimization and miniaturization of the development library or application , It will be reserved for all statements Kotlin Class to compose the new correct metadata .

Let's take a look at the changes in our example . We add the sample code to a Android Studio In the library project . stay gradle.build In file , By way of  minifyEnbled  Set up true To enable packet size reduction , We update the reducer configuration , Make it contain the following :

# Retain  D8CommandBuilder  And all its methods 
-keep class com.example.mylibrary.D8CommandBuilder {
  <methods>;
}
# Keep extension functions 
-keep class com.example.mylibrary.CommandBuilderKt {
  <methods>;
}
# Retain  kotlin.Metadata  Annotations to maintain metadata on retained items 
-keepattributes RuntimeVisibleAnnotations
-keep class kotlin.Metadata { *; }

The above tells R8 Retain  D8CommandBuilder  as well as  CommandBuilderKt  All extension functions in . It also tells R8 Keep the note , In especial  kotlin.Metadata  annotation . These rules only apply to classes that are explicitly declared reserved . therefore , Only  D8CommandBuilder  and  CommandBuilderKt  Metadata will be preserved . however  CommandBuilderBase  The metadata in Can't Reserved . We can reduce unnecessary metadata in application and development libraries by doing this .

Now? , Enable reduced Libraries , Inside  CommandBuilderBase  It was renamed  a. Besides , Of the reserved class Kotlin Metadata is also rewritten , So that's all for  CommandBuilderBase  References to are replaced with references to  a  References to . In this way, the development library can be used normally .

One final note , stay  CommandBuilderBase  We don't reserve Kotlin Metadata means Kotlin The compiler takes the generated class as Java Class to treat . This can lead to Kotlin Class Java Implementation details produce strange results . To avoid such problems , You need to keep the class . If you keep the class , The metadata will be preserved . We can use... In retention rules  allowobfuscation  Modifier to allow R8 Rename class , Generate Kotlin Metadata , such Kotlin The compiler and Android Studio Will regard this class as Kotlin class .

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

Come here , We talked about Library reduction and Kotlin Metadata for Kotlin The role of the development library . adopt kotlin-reflect Library usage Kotlin The application of reflection also needs Kotlin Metadata . The problems faced by application and development libraries are the same . If Kotlin Metadata is deleted or not updated correctly ,kotlin-reflect Libraries can't treat code as Kotlin Code to process .

A simple example , For example, we want to find and call an extension function in a class at run time . We want to enable method renaming , Because we don't care about function names , As long as you can find it at run time and call it .

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

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

In the code , We added a call to : reflect(ReflectOnMe()). It will find the definition in  ReflectOnMe  The extension function in , And use the incoming  ReflectOnMe  Instance as receiver ,"reflection"  Call it as an extension sink .

Now? R8 Can be correctly overridden in all reserved classes Kotlin Metadata , We can enable rewriting by using the following reducer configuration .

# Keep the reflective class and its methods 
-keep,allowobfuscation class ReflectOnMe {
  <methods>;
}
# Retain  kotlin.Metadata  Annotations to maintain metadata on retained items 
-keepattributes RuntimeVisibleAnnotations
-keep class kotlin.Metadata { *; }

This configuration causes the reducer to rename  ReflectOnMe  And extension functions at the same time , Still maintain and rewrite Kotlin Metadata .

Give it a try !

Welcome to try R8 about Kotlin In the library project Kotlin Metadata rewriting features , And in Kotlin Project use Kotlin Reflection . This feature can be found in Android Gradle Plugin 4.1.0-beta03 And later versions . If you encounter any problems in the process of use , Please in our  Issue Tracker Submit question page .

版权声明
本文为[Android Developers ]所创,转载请带上原文链接,感谢