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 .