本文原文出自 jakewharton 关于 D8
和 R8
系列文章第四篇。
在前面的三篇文章中,我们着重讲述了 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
配合使用。
评论