This is the 17th day of my participation in Gwen Challenge

This article is part four of jakewharton’s series on D8 and R8.

  • R8 Optimization: Staticization
  • Originally written by jakewharton
  • Translator: Antway

In the previous three articles, we focused on the functionality of D8. The core function of D8 is to convert Java bytecode to Dalvik bytecode, but it also involves the adaptation of new Java features and bugs from individual vendors or specific Android VM versions.

In general, D8 is not optimized. It is responsible for converting Java bytecode to more efficient Dalvik bytecode, such as the not-IN instruction we mentioned earlier, or for optimizing new Features of the Java language through desugar-adaptation. In addition to these very basic optimizations, D8 has some direct optimizations.

R8 is a version of D8, and its function is also related to optimization. It’s not a separate tool or code base, it’s a tool that runs in a more advanced mode. The first step performed by D8 optimization is to parse Java bytecode to the intermediate presentation layer (IR), and then R8 optimizes before writing Dalvik bytecode.

This article explores some of the optimizations performed by the R8 tool, which are similar to the properties of static, hence the name “Staticization.”

1. Companion Objects

Kotlin uses Companion Objects to emulate static modifiers in Java. This is an important language feature that can implement inheritance or interface functionality, so whether or not we use it to emulate static in development is expensive.

fun main(vararg args: String) {
  println(Greeter.hello().greet("Olive"))}class Greeter(val greeting: String) {
  fun greet(name: String) = "$greeting.$name!"

  companion object {
    fun hello(a) = Greeter("Hello")}}Copy the code

In this case, the Greeter class Greeter is used to generate a Greeter object using the greet method of the companion object in the main method.

We compiled it with the kotlinc instruction, packaged it into dx with D8, and finally looked at the bytecode of the dex file with dexdump.

$ kotlinc *.kt

$ java -jar d8.jar \
    --lib $ANDROID_HOME/platforms/android-28/android.jar \
    --release \
    --output . \
    *.class

$ $ANDROID_HOME/ build - the tools / 28.0.3 / dexdump - d classes. Dex... [000370] GreeterKt.main:([Ljava/lang/String;)V 0000: sget-object v1, LGreeter;.Companion:LGreeter$Companion;
0002: invoke-virtual {v1}, LGreeter$Companion; .hello:()LGreeter; 0005: move-result-object v1 0006: const-string v0,"Olive"0008: invoke-virtual {v1, v0}, LGreeter; .greet:(Ljava/lang/String;) Ljava/lang/String; 000b: move-result-object v1 000c: sget-object v0, Ljava/lang/System; .out:Ljava/io/PrintStream; 000e: invoke-virtual {v0, v1}, Ljava/io/PrintStream; .println:(Ljava/lang/Object;) V: 0011 return - void...Copy the code

At position 0000, an instance of Greeter$Companion is created and its Hello method is called at position 0002.

Let’s look at the bytecode of the nested Companion class to see if it has any virtual references.

Virtual methods   -
  #0 : (in LGreeter$Companion;)
    name          : 'hello'
    type          : '()LGreeter; 'access : 0x0011 (PUBLIC FINAL) [000314] Greeter.Companion.hello:(Ljava/lang/String;) Ljava/lang/String; 0000: new-instance v0, LGreeter; 0002: const-string v1,"Hello"0004: invoke-direct {v0, v1}, LGreeter; .<init>:(Ljava/lang/String;) V 0007: return-object v0Copy the code

The use of the Companion Object in the Greeter class gives rise to a new Companion class that increases the size of the binary bytecode and slows loading due to additional classes. The Companion class also increases memory stress by taking up memory throughout the life of the application. Finally, instance method calls require virtual references to be slower than static method calls. Of course, the impact of all this on a class is very small, but in a large application written entirely in Kotlin, the performance overhead is significant.

We can compile Java bytecode to Dalvik bytecode with the R8 directive. R8 is used in a similar way to D8, but R8 needs to specify –pg-conf to support obtrude. Here we need to declare the obfuscation file to prevent the main method from being obfuscated.

$ cat rules.txt
-keepclasseswithmembers class * { 
  public static void main(java.lang.String[]); 
}
-dontobfuscate

$ java -jar r8.jar \
    --lib $ANDROID_HOME/platforms/android-28/android.jar \
    --release \
    --output . \
    --pg-conf rules.txt \
    *.class
Copy the code

R8 will also generate a dex file similar to D8, but the source code of dex file is not optimized.

$ $ANDROID_HOME/ build - the tools / 28.0.3 / dexdump - d classes. Dex... [000234] GreeterKt.main:([Ljava/lang/String;)V 0000: invoke-static {}, LGreeter;.hello:()LGreeter; 0003: move-result-object v1 0004: const-string v0,"Olive"0006: invoke-virtual {v1, v0}, LGreeter; .greet:(Ljava/lang/String;) Ljava/lang/String; 0009: move-result-object v1 000a: sget-object v0, Ljava/lang/System; .out:Ljava/io/PrintStream; 000c: invoke-virtual {v0, v1}, Ljava/io/PrintStream; .println:(Ljava/lang/Object;) V 000 f: return - void...Copy the code

The main method is much cleaner than the previous version, using the invoke-static directive instead of getting the Companion example through sget-Object and making an invoke-virtual call. Note that R8 does not introduce the declaration of hello as static. Instead, it moves the hello method directly from Companion into the Greeter class.

  #1 : (in LGreeter;)
    name          : 'hello'
    type          : '(Ljava/lang/String;) Ljava/lang/String; 'access : 0x0019 (PUBLIC STATIC FINAL) [0002bc] Greeter.hello:(Ljava/lang/String;) Ljava/lang/String; [000240] Greeter.hello:()LGreeter; 0000: new-instance v0, LGreeter; 0002: const-string v1,"Hello"0004: invoke-direct {v0, v1}, LGreeter; .<init>:(Ljava/lang/String;) V 0007: return-object v0Copy the code

After the Hello method moves, the entire Companion class and the Greeter instance it holds are removed. R8 finds methods that don’t actually need an instance to be called and converts them to static.

2. Source Transformation

It’s a challenge to understand exactly how Companion is represented in Kotlin, and how the optimization of R8 works in bytecode. To better understand these two aspects, we can simulate them at the source code level.

The bytecode compiled by the Kotlin compiler into the Greeter class looks like the following format.

public final class Greeter {
  public static final Companion Companion = new Companion();

  private final String greeting;

  public Greeter(String greeting) {
    this.greeting = greeting;
  }

  public String getGreeting(a) {
    return greeting;
  }

  public String greet(String name) {
    return greeting + "," + name;
  }

  public static final class Companion {
    private Companion(a) {}

    public Greeter hello(a) {
      return new Greeter("Hello"); }}}Copy the code

The constructor parameter val GREETING: String is converted here to a private field greeting. Companion Object Companion is a statically nested class that has been converted to Greeter. Let’s create a new class called GreeterKt to hold the main method.

public final class GreeterKt {
  public static void main(String[] args) {
    System.out.println(Greeter.Companion.hello().greet("Olive")); }}Copy the code

The Greeter object is generated by calling the Hello method with a static Companior in the main method.

Let’s look at the optimization of R8.

- public final class Greeter {-public static final Companion Companion = new Companion();
-
   private finalString greeting; @ @ -public static final class Companion {-private Companion(a) {}
-
-    public Greeter hello(a) {-return new Greeter("Hello"); -} -} +public static Greeter hello(a) {+return new Greeter("Hello"); +}}Copy the code

The Hello method was optimized to Greeter’s static method, and Companion was removed.

public final class GreeterKt {
   public static void main(String[] args) {
-    System.out.println(Greeter.Companion.hello().greet("Olive"));
+    System.out.println(Greeter.hello().greet("Olive")); }}Copy the code

The main function has also been optimized to look like it was written in Java.

3. @JvmStatic

If you’re familiar with Kotlin and his Java interoperability story, you can use the @jVMState annotation to achieve a similar effect.

   companion object{+@JvmStatic
     fun hello(a) = Greeter("Hello")
Copy the code

We compiled the above example through D8.

$ kotlinc *.kt

$ java -jar d8.jar \
    --lib $ANDROID_HOME/platforms/android-28/android.jar \
    --release \
    --output . \
    *.class

$ $ANDROID_HOME/ build - the tools / 28.0.3 / dexdump - d classes. Dex...#2 : (in LGreeter;)
    name          : 'hello'
    type          : '()LGreeter; 'access : 0x0019 (PUBLIC STATIC FINAL) [00042c] Greeter.hello:()LGreeter; 0000: sget-object v0, LGreeter; .Companion:LGreeter$Companion;
0002: invoke-virtual {v0, v1}, LGreeter$Companion; .hello:()LGreeter; 0005: move-result-object v1 0006: return-object v1...Copy the code

As you can see from the bytecode above, the Hello method is added to the Greeter class as a static method, but the internal hello method is still called by the Companion instance.

[000234] GreeterKt.main:([Ljava/lang/String;)V
0000: sget-object v1, LGreeter;.Companion:LGreeter$Companion;
0002: invoke-virtual {v1}, LGreeter$Companion; .hello:()LGreeter; ...Copy the code

Even if static methods exist, Kotlin will still call the companion Object instance.

Even with @jVMstatic, R8 is still optimized statically by moving the Companion greet method into Greeter as a static method and calling the static method in main. And the entire Companoion class will be deleted.

4. More Than Companions

R8 is optimized not only for Companion Objects, but also for regular object objects.

@Module
object HelloGreeterModule {
  @Provides fun greeter(a) = Greeter("Hello")}Copy the code

Java bytecode is also optimized for useless instances.

public final class Thing {
  public static final Thing INSTANCE = new Thing();

  private Thing(a) {}

  public void doThing(a) {
    / /...}}Copy the code

This example is left as an exercise for the reader.

5. To summarize

In summary, statics optimizes static methods that do not require an instance to be invoked. For Kotlin, the R8 is well optimized for companion Objects. While R8 is also optimized for many of Kotlin’s specific bytecode patterns, stay tuned for the next article, which provides another R8 optimization that works well with Kotlin.