这是我参与更文挑战的第 17 天,活动详情查看: 更文挑战
本文原文出自 jakewharton 关于 D8 和 R8 系列文章第四篇。
- 原文链接 : R8 Optimization: Staticization
- 原文作者 : jakewharton
- 译者 : Antway
在前面的三篇文章中,我们着重讲述了 D8 的功能,D8 的核心功能是将 Java 字节码转换为 Dalvik 字节码,也会涉及到 Java 新特性的适配和个别供应商或特定 Android VM 版本的 bug。
一般来说,D8 不进行优化,它负责将 Java 字节码转换为更有效率的 Dalvik 字节码,比如我们前面提到的 not-in 指令,或者将 Java 语言新特性通过脱糖适配,在适配的过程中进行优化。除了这些非常基础的优化之外,D8 还有一些直接优化操作。
R8 是 D8 的一个版本,它的功能也是进行相关优化。它不是一个单独的工具或代码库,它是在更高级的模式下运行的一个工具。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 字节码,R8 跟 D8 的使用很相似,但是 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 在这里被转换为一个私有的字段 greeting。companion 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 仍然会进行静态优化,将 Companion 的 greet 方法移动到 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 配合使用。


















![[02/27][官改] Simplicity@MIX2 ROM更新-一一网](https://www.proyy.com/wp-content/uploads/2020/02/3168457341.jpg)



![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)