这是我参与更文挑战的第 29 天,活动详情查看: 更文挑战
原文出自 jakewharton 关于 D8 和 R8 系列文章第 16 篇。
- 原文链接 : D8 Optimization: Assertions
- 原文作者 : jakewharton
- 译者 : Antway
assert 关键字是用于测试不合法的 Java 语法。也就是说:你所期望的总是真实的。
它的语法有两种格式:
assert <bool-expression>;
assert <bool-expression> : <expression>;
只有在 JVM 上设置了 -ea(enable assertions)标志时,才会在运行时计算第一个表达式。第二个表达式(如果存在)用作断言错误构造函数的参数,该构造函数在第一个表达式返回 false 时抛出。
作为一名 Android 开发者可能对 assert 不是很熟悉,这是因为每个 Androd 进程都是来自 zygote 进程,而该进程已经禁用 assert。所以即使你在代码中使用 assert,它也是毫无效果的。
那为什么要费心谈论它呢?事实证明,它们将第一次在Android上变得有用!
1. 今天的行为
assert 语句保护必须始终为真的内容,以便程序正确执行。让我们写一个。
class IdGenerator {
private int id = 0;
int next() {
assert Thread.currentThread() == Looper.getMainLooper().getThread();
return id++;
}
}
这个类创建唯一的 id,并通过只允许来自主线程的调用来保证它们的唯一性。如果这个类是从多个线程并发调用的,您可能会看到重复的 id 值。当然,它有点做作,还有像 @MainThread 这样的东西是由 Lint 检查的,但是我们关注的是 assert,所以继续吧。
在 Null Data Flow Analysis 这篇文章中,我们介绍了 R8 通过 SSA 来优化代码分支,从 Java 字节码解析 next() 方法时的 SSA 大致如下所示:
D8 知道 Android 不支持 Java 断言。它将删除检查并用 false 替换它,从而允许消除死代码。这将传播到只有在返回 true 时才能获取的节点。
结果,布尔表达式和可选消息表达式从字节码中完全消除。只保留字段
read、field、increment 和 return。
我们可以通过编译
Java 源代码来确认这一点:
$ javac -bootclasspath $ANDROID_HOME/platforms/android-29/android.jar IdGenerator.java
$ java -jar $R8_HOME/build/libs/d8.jar \
--lib $ANDROID_HOME/platforms/android-29/android.jar \
--output . \
IdGenerator.class
$ dexdump -d classes.dex
⋮
[00011c] IdGenerator.next:()I
0000: iget v0, v2, LIdGenerator;.id:I
0002: add-int/lit8 v1, v0, #int 1
0004: iput v1, v2, LIdGenerator;.id:I
0006: return v0
⋮
消除运行时检查并且返回 false 很容易做到,但是 SSA 的方式意味着我们消除 assert 语句的两个表达式的字节码,包括它们所依赖的任何中间值。
2. 明天的行为
AGP 4.1 中的 D8 版本稍微改变了 Java assert 的处理。通过在编译时的检查来替换原来的在运行时检查。
实际上,这意味着任何调试变量都会开启编译时的断言 check 功能。
这去除了启用的检查,但保留不变的检查。
通过给
D8 指定 --force-enable-assertions (该指令在被 AGP 在 Debug 时自动添加)来编译 IdGenerator 类。
$ java -jar $R8_HOME/r8/build/libs/d8.jar \
--lib $ANDROID_HOME/platforms/android-29/android.jar \
+ --force-enable-assertions \
--output . \
IdGenerator.class
$ dexdump -d classes.dex
⋮
[000190] IdGenerator.next:()I
+0000: invoke-static {}, Ljava/lang/Thread;.currentThread:()Ljava/lang/Thread;
+0003: move-result-object v0
+0004: invoke-static {}, Landroid/os/Looper;.getMainLooper:()Landroid/os/Looper;
+0007: move-result-object v1
+0008: invoke-virtual {v1}, Landroid/os/Looper;.getThread:()Ljava/lang/Thread;
+000b: move-result-object v1
+000c: if-ne v0, v1, 0015
000e: iget v0, v2, LIdGenerator;.id:I
0010: add-int/lit8 v1, v0, #int 1
0012: iput v1, v2, LIdGenerator;.id:I
0014: return v0
+0015: new-instance v0, Ljava/lang/AssertionError;
+0017: invoke-direct {v0, v1}, Ljava/lang/AssertionError;.<init>:()V
+001a: throw v0
我们的调试构建仍然在运行时测试不变量,但是发布构建完全消除了检查。这种行为现在类似于 JVM,在 JVM 中,单元测试启用 -ea 标志,而生产则不启用。
(如果您想知道为什么抛出异常的代码被移到方法的底部,请通过这篇 Optimizing Bytecode by Manipulating Source Code 文章了解。)
这些特性在最新的 AGP 4.1 alphas 版本上实现,不变量的本质是,除非你已经做了非常错误的事情,否则它们永远不会失败。通过在调试版本中检查它们,我们有信心在 Android 运行时获得库和应用程序代码的正确性。
Kotlin 的 assert() 函数与 Java 的 assert 关键字相比,在行为上存在细微的差异。更多的信息可以参照 Jesse Wilson 的 Kotlin’s Assert Is Not Like Java’s Assert 这篇文章。D8 目前无法识别 Kotlin 的 assert() 来做优化,但由于这个原因,关于 D8 的这个 original D8 feature request 仍然没有关闭。
与最近文章中介绍的一些 R8 优化不同,这种优化只局限于单个方法的主体,这就是为什么 D8 也可以执行它的原因。查看 D8 优化的文章 D8 Optimizations,了解更多适用于 D8 和 R8 的优化。
敬请期待更多的 D8 和 R8 优化帖子即将发布!