写点什么

[译] R8 优化: Staticization

用户头像
Antway
关注
发布于: 2021 年 06 月 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;)V0000: sget-object v1, LGreeter;.Companion:LGreeter$Companion;0002: invoke-virtual {v1}, LGreeter$Companion;.hello:()LGreeter;0005: move-result-object v10006: const-string v0, "Olive"0008: invoke-virtual {v1, v0}, LGreeter;.greet:(Ljava/lang/String;)Ljava/lang/String;000b: move-result-object v1000c: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream;000e: invoke-virtual {v0, v1}, Ljava/io/PrintStream;.println:(Ljava/lang/Object;)V0011: 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;)V0007: 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;)V0000: invoke-static {}, LGreeter;.hello:()LGreeter;0003: move-result-object v10004: const-string v0, "Olive"0006: invoke-virtual {v1, v0}, LGreeter;.greet:(Ljava/lang/String;)Ljava/lang/String;0009: move-result-object v1000a: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream;000c: invoke-virtual {v0, v1}, Ljava/io/PrintStream;.println:(Ljava/lang/Object;)V000f: 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;)V0007: 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 v10006: return-object v1
复制代码


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


[000234] GreeterKt.main:([Ljava/lang/String;)V0000: 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 对象也进行优化。


@Moduleobject 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 配合使用。

用户头像

Antway

关注

持续精进,尽管很慢 2019.05.27 加入

专注开源库

评论

发布
暂无评论
[译] R8 优化: Staticization