[译] R8 优化:Staticization(静态)

这是我参与更文挑战的第 17 天,活动详情查看: 更文挑战

本文原文出自 jakewharton 关于 D8R8 系列文章第四篇。

在前面的三篇文章中,我们着重讲述了 D8 的功能,D8 的核心功能是将 Java 字节码转换为 Dalvik 字节码,也会涉及到 Java 新特性的适配和个别供应商或特定 Android VM 版本的 bug

一般来说,D8 不进行优化,它负责将 Java 字节码转换为更有效率的 Dalvik 字节码,比如我们前面提到的 not-in 指令,或者将 Java 语言新特性通过脱糖适配,在适配的过程中进行优化。除了这些非常基础的优化之外,D8 还有一些直接优化操作。

R8D8 的一个版本,它的功能也是进行相关优化。它不是一个单独的工具或代码库,它是在更高级的模式下运行的一个工具。D8 优化执行的第一步是解析 Java 字节码到中间表示层(IR),然后 R8 在写出 Dalvik 字节码之前进行优化。

本文将探索 R8 工具执行的一些优化,这些优化跟 static 的特性很类似,所以本文取名为「Staticization」。

1. Companion Objects

Kotlin 中使用 Companion Objects 来模拟 Java 中的 static 修饰符,这是一个非常重要的语言特性,它可以实现继承或实现接口的功能,所以在开发中不管我们是否用于模仿 static,它的消耗都是很大。

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() = Greeter("Hello")
  }
}
复制代码

在这个例子中,Greeter 类里面通过 companion object 定义了函数 hello 用于生成一个 Greeter 对象,同时在 main 方法中调用 Greeter 对象的 greet 方法。

我们通过 kotlinc 指令编译,并且通过 D8 打包成 dx,最后通过 dexdump 查看 dex 文件的字节码。

$ kotlinc *.kt

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

$ $ANDROID_HOME/build-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
…
复制代码

在位置 0000 处,创建了一个 Greeter$Companion 实例,在 0002 处调用了该实例的 hello 方法。

我们查看下嵌套类 Companion 的字节码,看看它是否有虚拟引用。

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 v0
复制代码

Greeter 类中使用 companion object 产生了一个新的 Companion 类,它增加了二进制字节码的大小,同时因为额外类的加载会使加载变慢。同时 Companion 类会在应用的整个生命周期都占用内存会增加内存压力。最后,实例方法的调用需要虚拟引用比静态方法的调用慢。当然,所有这些东西对一个类的影响非常小,但是在一个完全用 Kotlin 编写的大型应用程序中,它的性能开销很大。

我们可以通过 R8 指令来编译 Java 字节码为 Dalvik 字节码,R8D8 的使用很相似,但是 R8 需要指定 --pg-conf 用来支持混淆。这里我们需要声明混淆文件,防止 main 方法被混淆。

$ 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
复制代码

R8 也会产生个跟 D8 类似的 dex 文件,只不过 dex 文件的源码没有被优化。

$ $ANDROID_HOME/build-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
000f: return-void
…
复制代码

相比较以前的版本 main 方法简洁了很多,通过使用 invoke-static 指令替代了原来通过 sget-object 获取 Companion 示例并进行 invoke-virtual 调用的方案。需要注意的是,R8 并不是引入了将 hello 方法声明为 static,而是直接将 hello 方法从 Companion 移动到 Greeter 类中。

  #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 v0
复制代码

hello 方法移动后,Companion 整个类以及它持有的 Greeter 实例都被删除了。R8 找到那些实际上不需要实例才能调用的方法,将它们转换为 static 类型。

2. Source Transformation

准确理解 Kotlin 中的 Companion 是如何表示的,以及 R8 的优化在字节码中是如何工作的,这是一个挑战。为了更好地理解这两个方面,我们可以在源代码级别模拟它们。

Kotlin 编译器编译 Greeter 类后的字节码如同下面的格式。

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() {
    return greeting;
  }

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

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

    public Greeter hello() {
      return new Greeter("Hello");
    }
  }
}
复制代码

构造方法中的参数 val greeting: String 在这里被转换为一个私有的字段 greetingcompanion object Companion 被转换为 Greeter 的静态嵌套类。我们新建一个名为 GreeterKt 的类用来存放 main 方法。

public final class GreeterKt {
  public static void main(String[] args) {
    System.out.println(Greeter.Companion.hello().greet("Olive"));
  }
}
复制代码

main 方法中通过静态的 Companior 调用 hello 方法生成 Greeter 对象。

看一下 R8 的优化。

- public final class Greeter {
-  public static final Companion Companion = new Companion();
-
   private final String greeting;
@@

-  public static final class Companion {
-    private Companion() {}
-
-    public Greeter hello() {
-      return new Greeter("Hello");
-    }
-  }
+  public static Greeter hello() {
+    return new Greeter("Hello");
+  }
 }
复制代码

hello 方法优化为 Greeter 的静态方法,并且 Companion 被删除了。

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"));
   }
 }
复制代码

同样 main 函数也进行了优化,看起来就像 Java 写的一样。

3. @JvmStatic

如果您熟悉 Kotlin 及其 Java 互操作性的故事,可以使用 @JVMSTATE 注释来实现类似的效果。

   companion object {
+    @JvmStatic
     fun hello() = Greeter("Hello")
复制代码

我们通过 D8 编译上面的例子。

$ kotlinc *.kt

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

$ $ANDROID_HOME/build-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
…
复制代码

从在上面的字节码中可以看到 hello 方法被添加到 Greeter 类中作为一个静态方法,但是 hello 方法内部调用还是通过 Companion 实例调用的 hello 方法。

[000234] GreeterKt.main:([Ljava/lang/String;)V
0000: sget-object v1, LGreeter;.Companion:LGreeter$Companion;
0002: invoke-virtual {v1}, LGreeter$Companion;.hello:()LGreeter;
…
复制代码

即使存在静态方法,Kotlin 仍然会 companion object 实例调用方法。

即使使用 @JvmStatic 注解,R8 仍然会进行静态优化,将 Companiongreet 方法移动到 Greeter 中作为静态的方法,同时 main 函数中调用 static 方法,以及整个 Companoion 类会被删除。

4. More Than Companions

R8 不仅仅针对 companion object 进行优化,对于常规的 object 对象也进行优化。

@Module
object HelloGreeterModule {
  @Provides fun greeter() = Greeter("Hello")
}
复制代码

对于无用的实例,Java 字节码同样会进行优化。

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

  private Thing() {}

  public void doThing() {
    // …
  }
}
复制代码

这个例子就留给读者作为练习了。

5. 总结

总之,静态化使那些不需要通过实例才能调用的方法优化为 static 方法。对于 Kotlin 来说,R8 能够针对 companion object 进行很好的优化。同时 R8 也对很多 Kotlin 特定的字节码模式进行优化,请继续关注下一篇文章,它提供了另一个 R8 优化,可以很好地与 Kotlin 配合使用。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享