Shrinking Kotlin libraries & apps using Reflection with R8

Shrinking Kotlin libraries & apps using Reflection with R8
Shrinking Kotlin libraries & apps using Reflection with R8

R8 is the in-built application shrinker present in Google Android Studio. As advent goes on, R8 is now backed by Google for maintaining and rewriting it’s metadata so that it can be fully compatible with the shrinking of its libraries and the applications of Kotlin reflection.

The prime function of R8 is to reduce the size of Android applications by implementing pruning strategies. Pruning means to eliminate any branch of the code according to the laid constraints. R8 prunes the source code by eliminating the unused part of the code, such as functions that are never invoked, unused variables, classes with no objects. Apart from code elimination R8 also optimises the code that is retained.

On top of this, R8 is also programmed for shrinking Android libraries. This reduces the space complexity of the libraries and eventually our Android project. Library shrinking can also be used for abstraction. For instance, if any of the new features of your library is in the testing phase, or you are not ready to launch it publically, you can use Library shrinking for abstracting it.

The Android Gradle Plugin version 4.1.0-beta03 is the minimum requirement for implementing R8 libraries. It has been a boon for the Kotlin Developers, as they realised the need for metadata and the optimisation of networking strategies using them.

It is one of the most efficient programming languages for writing Android applications and libraries. Still, there are a lot of challenges while shrinking Kotlin libraries or applications that use Kotlin reflection. This is because to identify Kotlin language constructs, it uses metadata in Java class files. If your application shrinker is inefficient in maintaining and updating its metadata, your library or application will not compile successfully and will generate bug reports.

Kotlin Metadata

The supplementary information stored in annotations in Java class files which are produced by it’s JVM compiler is known as Kotlin metadata. The most important function of metadata is to specify which Kotlin language constructs a particular method or class present in the corresponding class file. For instance, the Kotlin compiler is allowed to recognise whether a method present in a class file is actually a Kotlin extension function with the help of Kotlin metadata only.

Go through the simple example depicted below with the help of a code snippet from GitHub. The depicted library code is used for defining a hypothetical base command builder for initiating 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
} Code Courtesy : CommandBuilder.kt hosted by GitHub   R8 allows us to define a hypothetical yet stable D8CommandBuilder on top of CommandBuilderBase so that the D8 command is simplified.   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 } Code Courtesy : DBCommandBuilder.kt hosted by GitHub        

The above mentioned example depicts the use of extension functions for ensuring that in case you  the setMinApi method is invoked by a D8CommandBuilder, the return type of the object will be altered to D8CommandBuilder and will not be CommandBuilderBase.

The extension function used in the above code snippet is placed on the file class CommandBuilderKt and they are on the top most layer of the hierarchy. Consider the following class file from the Git repository which uses javap output.

$ javap com/example/mylibrary/CommandBuilderKt.class
Compiled from “CommandBuilder.kt”
public final class CommandBuilderKt {
public static final T addInput(T, String);
public static final T setMinApi(T, int);

}

The extension functions used for compiling static methods that take an additional first parameter that is the extension receiver is represented by the javap output. Although, the set of information provides by the javap output is not sufficient for the Kotlin compiler to interfere whether these methods can be invoked from its code as extension functions or not. Hence, as a mandatory overhead, the Kotlin compiler appends the Metadata annotation in the Java class file. The additional Kotlin-specific information related to the class is present in this annotation. If you want to view the annotations, use the verbose flag along with javap.

You may notice a “d1” field in the metadata annotation; it contains the maximum real metadata in the form of a protocol buffer message. Although, the precise contents of the metadata is not considered to be too important. The most significant fact is that the Kotlin compiler employees this metadata for figuring out whether the methods are extension functions or not.

How R8 used to break Kotlin libraries?

After going through the previous section, we can conclude that the Kotlin metadata for the class files present in a library is totally necessary for implementing the Kotlin API of the library. Still, the R8 know nothing about metadata and there are considered to be an annotation and usually encoded as a protocol buffer message, which is completely abstract.

Hence, R8 performs one of the following two strategies:

  • Discard the metadata.
  • Retain the original metadata.

Unfortunately, both of these options are not feasible.

  • In case the metadata is discarded, it’s compiler will not be able to identify the nature of functions, such as the compiler won’t be aware that those extension functions are extension functions. Therefore, with respect to our example, as soon as the compiler encounters code such as D8CommandBuilder().setMinApi(12), it will generate several bugs and state,” no such method exists”. The compiler generates these errors, as in the absence of metadata; the Kotlin compiler considers it to be a usual Java static method with two parameters.
  • The second case,  that is, retaining the original metadata is equally bad. The supertypes for classes are also one of the contents of the Kotlin metadata. Now, in case the developer wants to retain the names of the D8CommandBuilder class while shrinking the library. This would lead to the renaming of the CommandBuilderBase  to “a”.

The Kotlin compiler seeks for the supertype of D8CommandBuilder in the Kotlin metadata records, if the original Kotlin metadata is withdrawn. Instead, if the original metadata is used the recorded supertype is CommandBuilderBase , rather than “a”. Hence, the compilation fails and displays an error message stating that the supertype CommandBuilderBase does not exist.

R8 Kotlin Metadata rewriting

For avoiding the issues associated with the above mentioned two cases, Google has extended the capability of R8.It now has the ability to maintain and rewrite Kotlin metadata. This was done by including the Kotlin metadata library developed by JetBrains in R8.The Kotlin metadata in the original input can be read by using the metadata library. The data extracted from the metadata records is stored in an internal data structure of the R8.After the R8 is done with library shrinking and optimisation; it generates new accurate Kotlin metadata for the entire range of explicitly kept Kotlin classes.

Finally, if you don’t store the Kotlin metadata on CommandBuilderBase the Kotlin compiler automatically treats the resultant class in the output to be a Java class. This can add strange functionalities in your library used for Java implementation details of the Kotlin class. For escaping this, retain the class, so that the metadata is also retained. The allowobfuscation modifier can be used for renaming the class to minimize ambiguity.

We have been through the use of metadata with the help of the code snippets and relevant Java examples. Kotlin libraries are driven by Kotlin Metadata. Along with that, the applications that use Kotlin reflection through the Kotlin-reflect library also require Kotlin metadata. Although, there are some issues faced by Kotlin-reflect library, analogously to the Kotlin libraries. In case the metadata is discarded or not updated in the real-time, the Kotlin-reflect library will generate bugs while compiling the code.

After knowing the advantages of Kotlin R8, even the usual developers from the Android developers’ community will get motivated to their hands-on creating libraries. For creating full-fledged live applications, hundreds of libraries are required for designing components, networking, and view elements and so on. Sometimes, you even need to add libraries for components as simple as a scratch view or a shimmer layout. The sizes of these libraries added an extra over-head memory to the application size, therefore R8 was introduced.R8 can be noted as a remarkable success in the field of Library development in Android, as it reduces the size of libraries to approximately half.

To read more about Kotlin, click here.

By Vanshika Singolia