这是我参与更文挑战的第 18 天,活动详情查看: 更文挑战
本文原文出自 jakewharton 关于 D8 和 R8 系列文章第五篇。
- 原文链接 : R8 Optimization: Null Data Flow Analysis (Part 1)
- 原文作者 : jakewharton
- 译者 : Antway
上篇文章中第一次介绍到 R8 的优化,本篇文章将介绍 R8 针对 Null Data(空数据)的优化,让我们一起开始吧!
1. R8
fun <T : Any> coalesce(a: T?, b: T?): T? = a ?: b
fun main(vararg args: String) {
println(coalesce("one", "two"))
println(coalesce(null, "two"))
}
复制代码
在上面示例中,coalesce 函数根据参数 a 是否为 null 来进行返回,如果 a 不为 null,则返回 a,反之返回 b。上面的输出结果如下:
one
two
复制代码
在编译时,如果一个函数的函数体很简短,R8 和 ProGuard 会在该函数的调用处将函数体内置到调用函数中。因为 coalesce 很简短,所以它的函数体会被内联嵌套在所有调用它的地方。
fun main(vararg args: String) {
println("one" ?: "two")
println(null ?: "two")
}
复制代码
实际上,Kotlin 编辑器能够在编译时期确认 ?: 操作符运算结果,我们编译上面的代码,查看编译打包后的字节码来验证。
[000180] NullsKt.main:([Ljava/lang/String;)V
0000: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;
0002: const-string v0, "one"
0004: invoke-virtual {v1, v0}, Ljava/io/PrintStream;.println:(Ljava/lang/Object;)V
0007: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;
0009: const-string v0, "two"
000b: invoke-virtual {v1, v0}, Ljava/io/PrintStream;.println:(Ljava/lang/Object;)V
000e: return-void
复制代码
如我们所料,字节码中并没有进行条件判断,而是直接调用 println 方法输出 one 和 two。但是,由于优化发生在 R8 内部,而不是在 Kotlin 编译器之前,所以实际的 Dalvik 字节码包含条件。
[000144] NullsKt.main:([Ljava/lang/String;)V
0000: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;
0002: const-string v0, "one"
0004: if-nez v0, 0006
0006: const-string v0, "two"
0008: invoke-virtual {v1, v0}, Ljava/io/PrintStream;.println:(Ljava/lang/Object;)V
000b: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;
000d: const/4 v0, #int 0
000f: if-nez v0, 0010
0010: const-string v0, "two"
0012: invoke-virtual {v1, v0}, Ljava/io/PrintStream;.println:(Ljava/lang/Object;)V
0015: return-void
复制代码
在 0002 处加载常量 one,然后 0004 判断是否为空,所以一直是不为空的,这样就导致 0006 处的代成成为 dead code,无法加载 two。同样的道理对于 000d 处加载 0(代表 null),然后在 000f 处进行非空检查,这个会一直失败,因为一直为空,然后直接去执行 0010 处的代码。
在上一篇文章中,我们讲到 R8 在 IR 层面优化代码,IR 使用 SSA 来加强一些优化项。使用 SSA,R8 可以决定程序中的数据的处理流程。对于上面代码第一个 println 的内联数据处理流程可简要描述如下。

SSA 的基本特性是每个变量只赋值一次。这也是字符串 two 赋值给 y 而不赋值给 x 的原因。z 通过 [Φ(欧拉函数)](https://baike.baidu.com/item/%E6%AC%A7%E6%8B%89%E5%87%BD%E6%95%B0) 来选择 x 或 y 的值。结合前面的字节码,可以看到 x、y 和 z 最后都会赋值给寄存器地址 v0,而寄存器地址 v0 会被覆盖。注意单次分配只针对 IR 层!
如果我们针对上面的流程图增加一些空信息,因为 x 和 y 都是用常量初始化的,所以它们一定不为空。进而,z 也是不为空的,
当 x 不为 null 时,R8 知道 if-nez 字节码检查 x 非空是恒成立的,所以这个判断是无用的。同样,针对 y 的赋值也是无用的。

通过上面的分析,我们知道 false 分支判断的字节码是无用的 dead-code,所以我们对分支进行优化,删除无用的判断分支。

从图中可以看到 z 的值就是 Φ 函数对单变量 x 的运算结果,所以我们可以将 z 替换为 x。

从图中可以看到剩下的部分分别是:一个指向 System.out 的 w 变量、将 one 赋值给 x 和 w 调用 println 函数输出 x。
上面的介绍是针对示例代码中的第一个 println 函数。第二个 println 函数由于初始化时是 null,所以跟上面的过程是相反的。x 初始化为 null,进行非空检查一直是 false,所以 y 一直是 two。

通过使用 SSA IR,R8 能够根据条件进行优化,删除无用代码。
$ kotlinc *.kt
$ 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 kotlin-stdlib-1.3.11.jar
$ $ANDROID_HOME/build-tools/28.0.3/dexdump -d classes.dex
[000340] NullsKt.main:([Ljava/lang/String;)V
0000: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;
0002: const-string v0, "one"
0004: invoke-virtual {v1, v0}, Ljava/io/PrintStream;.println:(Ljava/lang/Object;)V
0007: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;
0009: const-string v0, "two"
000b: invoke-virtual {v1, v0}, Ljava/io/PrintStream;.println:(Ljava/lang/Object;)V
000e: return-void
复制代码
通过上面的字节码可以看到完全和我们一开始的示例分析一样。
2. Analysis Inside D8(D8 内部分析)
针对上面的例子,我尝试使用 Java 代码进行实现来分析看看。
class Nulls {
public static void main(String... args) {
Object first = "one";
if (first == null) {
first = "two";
}
System.out.println(first);
Object second = null;
if (second == null) {
second = "two";
}
System.out.println(second);
}
}
复制代码
我们对上面的例子进行编译,然后通过 d8 进行打包,最后通过 dumpdex 查看 dex 字节码,发现判断条件仍然被消除了。
$ javac *.java
$ 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
[000224] Nulls.main:([Ljava/lang/String;)V
0000: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;
0002: const-string v0, "one"
0004: invoke-virtual {v1, v0}, Ljava/io/PrintStream;.println:(Ljava/lang/Object;)V
0007: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;
0009: const-string v0, "two"
000b: invoke-virtual {v1, v0}, Ljava/io/PrintStream;.println:(Ljava/lang/Object;)V
000e: return-void
复制代码
发生这种情况的原因是 D8 同样使用 IR 进行优化,并且仍然存在空信息。即使不进行任何 R8 优化,如果在 IR 中存在的判断条件始终为 true 或 false,D8 同样会进行相关代码的清除优化。
如果我们使用不包含 IR 的 dx 工具编译,则会发现字节码中仍然存在条件判断和无用的代码。
$ $ANDROID_HOME/build-tools/28.0.3/dx --dex --output=classes.dex *.class
$ $ANDROID_HOME/build-tools/28.0.3/dexdump -d classes.dex
[000204] Nulls.main:([Ljava/lang/String;)V
0000: const-string v0, "one"
0002: if-nez v0, 0006
0004: const-string v0, "two"
0006: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;
0008: invoke-virtual {v1, v0}, Ljava/io/PrintStream;.println:(Ljava/lang/Object;)V
000b: const/4 v0, #int 0
000c: if-nez v0, 0010
000e: const-string v0, "two"
0010: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;
0012: invoke-virtual {v1, v0}, Ljava/io/PrintStream;.println:(Ljava/lang/Object;)V
0015: return-void
复制代码
因此,R8 对内联嵌套进行优化确实有很好的作用,但是如果源代码中存在常量条件和死代码,D8 在编译时同样会消除它们。
3. 总结
这篇文章只触及 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)