写点什么

[译] D8 优化: Assertions

用户头像
Antway
关注
发布于: 2 小时前

原文出自 jakewharton 关于 D8R8 系列文章第 16 篇。



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 时才能获取的节点。



结果,布尔表达式和可选消息表达式从字节码中完全消除。只保留字段 readfieldincrementreturn



我们可以通过编译 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:()I0000: iget v0, v2, LIdGenerator;.id:I0002: add-int/lit8 v1, v0, #int 10004: iput v1, v2, LIdGenerator;.id:I0006: return v0
复制代码


消除运行时检查并且返回 false 很容易做到,但是 SSA 的方式意味着我们消除 assert 语句的两个表达式的字节码,包括它们所依赖的任何中间值。

2. 明天的行为

AGP 4.1 中的 D8 版本稍微改变了 Java assert 的处理。通过在编译时的检查来替换原来的在运行时检查。


实际上,这意味着任何调试变量都会开启编译时的断言 check 功能。



这去除了启用的检查,但保留不变的检查。



通过给 D8 指定 --force-enable-assertions (该指令在被 AGPDebug 时自动添加)来编译 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 运行时获得库和应用程序代码的正确性。


Kotlinassert() 函数与 Javaassert 关键字相比,在行为上存在细微的差异。更多的信息可以参照 Jesse WilsonKotlin’s Assert Is Not Like Java’s Assert 这篇文章。D8 目前无法识别 Kotlinassert() 来做优化,但由于这个原因,关于 D8 的这个 original D8 feature request 仍然没有关闭。


与最近文章中介绍的一些 R8 优化不同,这种优化只局限于单个方法的主体,这就是为什么 D8 也可以执行它的原因。查看 D8 优化的文章 D8 Optimizations,了解更多适用于 D8R8 的优化。


敬请期待更多的 D8R8 优化帖子即将发布!

用户头像

Antway

关注

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

专注开源库

评论

发布
暂无评论
[译] D8 优化: Assertions