本文原文出自 jakewharton 关于 D8 和 R8 系列文章第二篇。
本系列的第一篇文章探讨了 Android 对 Java 8 的支持。尽管 Android 对 Java 8 的语言特性和 API 的支持还没有完全覆盖。但是 D8 已经为我们提供了部分核心的语言特性支持。
上篇文章很多人反馈 Java 8 版本已经太旧了,Java 生态系统中的一部分部分在使用 Java 9 和 10 之后,开始迁移到 Java 11(Java 8 之后的第一个长期支持发布)。很高兴上篇文章能有读者反馈,因为这样的反馈才有了这篇文章。
随着 Java 版本发布的频率提高,Android 的年度发布时间表、对新语言特征和 API 的延迟支持让人感到难受。但是,事实上我们是否被 Java 8 所困扰?让我们看看 Java 8 以上的版本有哪些特性,以及看看 Android 工具链是如何起作用的。
1. Java 9
在近 2-3 年的发布计划中,Java 9 包含了一些语言特性,但是都不是像 Lambda 一样很重要,更多的是清理一些边缘 API。
1.1 Try With Resources
使用 try-with-resources 来简化代码,它允许你创建一个临时变量,例如 try (Closeable bar = foo.bar())。但是如果已经定义了一个 Closeable 对象,那么在定义一个新的变量是多余的。因此,如果已经有了有效的最终引用,那么这个版本允许您省略声明一个新变量。
import java.io.*;
class Java9TryWithResources { String effectivelyFinalTry(BufferedReader r) throws IOException { try (r) { return r.readLine(); } }}
复制代码
这个特性在 Java 中已经完全支持,所以 D8 在 dex 打包时也已经支持。
$ javac *.java
$ java -jar d8.jar \ --lib $ANDROID_HOME/platforms/android-28/android.jar \ --release \ --output . \ *.class
$ lsJava9TryWithResources.java Java9TryWithResources.class classes.dex
复制代码
不同于 Java 8 中的 lambda 和静态接口方法特性,Java 9 的特性已经在所有的 Android API 版本上都是支持的。
1.2 Anonymous Diamond
在 Java 7 中引入了钻石操作,钻石操作简化了泛型初始化,可以让代码更易读。
List<String> strings = new ArrayList<>();
复制代码
上面代码在 new ArrayList<>() 时没有指明 String 类型,这样的方式剔除了无用的声明,但它不能用于匿名的内部类。但是在 Java 9 中, 它可以与匿名的内部类一起使用,从而提高代码的可读性。
import java.util.concurrent.*;
class Java9AnonymousDiamond { Callable<String> anonymousDiamond() { Callable<String> call = new Callable<>() { @Override public String call() { return "Hey"; } }; return call; }}
复制代码
同样,上面的方式完全是在 Java 编译器中实现的,因此生成的字节码就好像是显式指定了 String 一样。我们通过 javap 查看编译后的字节码。
$ javac *.java
$ javap -c *.classclass Java9AnonymousDiamond { java.util.concurrent.Callable<java.lang.String> anonymousDiamond(); Code: 0: new #7 // class Java9AnonymousDiamond$1 3: dup 4: aload_0 5: invokespecial #8 // Method Java9AnonymousDiamond$1."<init>":(LJava9AnonymousDiamond;)V 8: areturn}
class Java9AnonymousDiamond$1 implements java.util.concurrent.Callable<java.lang.String> { final Java9AnonymousDiamond this$0;
Java9AnonymousDiamond$1(Java9AnonymousDiamond); Code: 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:LJava9AnonymousDiamond; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."<init>":()V 9: return
public java.lang.String call(); Code: 0: ldc #3 // String Hey 2: areturn}
复制代码
因为字节码中没有什么特殊的地方,D8 可以毫无问题地处理这个特性。
$ java -jar d8.jar \ --lib $ANDROID_HOME/platforms/android-28/android.jar \ --release \ --output . \ *.class
$ lsJava9AnonymousDiamond.java Java9AnonymousDiamond.class Java9AnonymousDiamond$1.class classes.dex
复制代码
显然,又一个语言特性 Android 已经可以在所有 API 版本中支持。
1.3 Private Interface Methods
在接口中,static 或 default 方法会由于重写导致重复的实现,如果这些方法是类的一部分而不是接口,则可以提取这些函数为私有函数。在 Java 9 中为接口添加了用 private 修饰的私有方法。
interface Java9PrivateInterface { static String hey() { return getHey(); }
private static String getHey() { return "hey"; }}
复制代码
这是 Java 9 中第一个需要支持的语言功能。在此版本之前,不允许在接口成员上使用 private。由于 D8 已经通过脱糖处理了 default 和 static 修饰符,private 方法很容易使用相同的技术兼容处理。
$ javac *.java
$ java -jar d8.jar \ --lib $ANDROID_HOME/platforms/android-28/android.jar \ --release \ --output . \ *.class
$ lsJava9PrivateInterface.java Java9PrivateInterface.class classes.dex
复制代码
在 API 24 的 ART 环境中已经支持 static 和 default 修饰符,当我们指定 --min-api 24 时,static 和 default 修饰的方法都不会进行脱糖。
$ $ANDROID_HOME/build-tools/28.0.2/dexdump -d classes.dexClass #1 - Class descriptor : 'LJava9PrivateInterface;' Access flags : 0x0600 (INTERFACE ABSTRACT) Superclass : 'Ljava/lang/Object;' Direct methods - #0 : (in LJava9PrivateInterface;) name : 'getHey' type : '()Ljava/lang/String;' access : 0x000a (PRIVATE STATIC)00047c: |[00047c] Java9PrivateInterface.getHey:()Ljava/lang/String;00048c: 1a00 2c00 |0000: const-string v0, "hey"000490: 1100 |0002: return-object v0
复制代码
通过查看 dex 文件的字节码,我们可以看到 getHey 方法仍然是 private 和 static 类型的,说明没有被脱糖。如果我们写个 main 方法调用 getHey,在 API 24 的机器上是可以正常运行,因为 ART 在 API 24 版本已经支持。
上面就是 Java 9 的语言特性,并且 Android 已经支持。但是 Java 9 中的 API 还没有被 Android 全面支持,比如 Process API、Variable Handles API、Reactive Streams API 等等。
1.4 String Concat
每次讨论 Java 版本的发布,我们讨论语言特性比较多,但是每个版本也会针对 bytecode 进行优化,比如 Java 9 中的字符串连接。
class Java9Concat { public static String thing(String a, String b) { return "A: " + a + " and B: " + b; }}
复制代码
究竟做了哪些优化,我们可以通过 Java 8 和 Java 9 的编译器进行对比。首先使用 Java 8 的 javac 进行编译字节码。
$ java -versionjava version "1.8.0_192"Java(TM) SE Runtime Environment (build 1.8.0_192-b12)Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12, mixed mode)
$ javac *.java
$ javap -c *.classclass Java9Concat { public static java.lang.String thing(java.lang.String, java.lang.String); Code: 0: new #2 // class java/lang/StringBuilder 3: dup 4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V 7: ldc #4 // String A: 9: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 12: aload_0 13: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 16: ldc #6 // String and B: 18: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: aload_1 22: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 25: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 28: areturn}
复制代码
我们可以看到,这里使用的是 StringBuilder 进行连接。如果我们用 Java 9 来编译字节码对比。
$ java -versionjava version "9.0.1"Java(TM) SE Runtime Environment (build 9.0.1+11)Java HotSpot(TM) 64-Bit Server VM (build 9.0.1+11, mixed mode)
$ javac *.java
$ javap -c *.classclass Java9Concat { public static java.lang.String thing(java.lang.String, java.lang.String); Code: 0: aload_0 1: aload_1 2: invokedynamic #2, 0 // InvokeDynamic #0:makeConcatWithConstants:( Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 7: areturn}
复制代码
一个 invokedynamic 指令就替代了 StringBuilder 的那么多操作,这个操作跟我们上一篇的 native lambdas work on the JVM 章节很类似。
在 JVM 的运行时期, JDK class StringConcatFactory 使用 makeConcatWithConstants 方法在连接字符时效率更好,比如不必重新编译以及可以预置 StirngBuilder 的大小。
Android API 没有包含 Java 9 中的太多 API,所以在运行时期还无法使用 StringConcatFactory 类,不过值得庆幸的是,正如 Android 对 lambda 的支持,D8已经通过脱糖实现优对 StringConcatFactory 的支持。
$ java -jar d8.jar \ --lib $ANDROID_HOME/platforms/android-28/android.jar \ --release \ --output . \ *.class
$ $ANDROID_HOME/build-tools/28.0.2/dexdump -d classes.dex[000144] Java9Concat.thing:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;0000: new-instance v0, Ljava/lang/StringBuilder;0002: invoke-direct {v0}, Ljava/lang/StringBuilder;.<init>:()V0005: const-string v1, "A: "0007: invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;000a: invoke-virtual {v0, v2}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;000d: const-string v2, " and B: "000f: invoke-virtual {v0, v2}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;0012: invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;0015: invoke-virtual {v0}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String;0018: move-result-object v20019: return-object v2
复制代码
这意味着 Java 9 的所有语言特征都可以在 Android 所有 API 级别上使用。
Java 现在每隔 6 个月发版一次,Java 9 已经算是老版本,Android 能保持同步进行吗?
2. Java 10
Java 10 中最大的语言特性是 local-variable type inference(局部变量接口),它允许我们使用 var 关键字定义变量来忽略类型。
import java.util.*;
class Java10 { List<String> localVariableTypeInferrence() { var url = new ArrayList<String>(); return url; }}
复制代码
通过 javac 编译来看:
$ javac *.java
$ javap -c *.classCompiled from "Java10.java"class Java10 { java.util.List<java.lang.String> localVariableTypeInferrence(); Code: 0: new #2 // class java/util/ArrayList 3: dup 4: invokespecial #3 // Method java/util/ArrayList."<init>":()V 7: areturn}
复制代码
针对这个特性在字节码中没有发现新的 API,所以 Android 也是完全支持的。当然,Java 10 版本中也有新的 API,比如 Optional.orElseThrow、List.copyOf和 CalcCurth.TunMuffFielabelist。一旦在未来将这些 API 添加到 Android SDK 中,这些 API 就可以通过脱糖来支持。
3. Java 11
Local-variable type inference(局部变量接口)在 Java 11 中得到了加强,它可以支持 lambda。
import java.util.function.*;
@interface NonNull {}
class Java11 { void lambdaParameterTypeInferrence() { Function<String, String> func = (@NonNull var x) -> x; }}
复制代码
和 Java 10 中的局部变量接口一样,Java 11 的这个特性也是被 Android 支持的。
Java 11 中提供的新 API 比如,String、Predicate.not 的辅助类以及 Reader、Writer、InputSteam 和 OutputStream 增加的空 IO 处理。
另一个 Java 11 中的重大变更 API 是 new HTTP client, java.net.http,其实这个 API 在 Java 9 的 jdk.incubator.http 包下已经可以试用。这个 API 系列非常庞大,Android 是否支持,我们拭目以待?
3.1 Nestmates(嵌套类)
在 Java 9 中针对字符连接进行了优化,那么 Java 11 中对长期存在的 Java 源代码与其类文件和 JVM 嵌套类之间的长期差异进行了修复。
在 Java 1.1 中引入了嵌套类,但是不符合类规范或 JVM 不识别,所以为了兼容这个问题,在源文件中的定义的嵌套类将按照一定的命名规则来创建一个源文件的兄弟类。
class Outer { class Inner {}}
复制代码
我们使用 Java 10 或以前的版本编译。
$ java -versionjava version "10" 2018-03-20Java(TM) SE Runtime Environment 18.3 (build 10+46)Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10+46, mixed mode)
$ javac *.java
$ lsOuter.java Outer.class Outer$Inner.class
复制代码
我们可以看到生成了两个字节码文件,对于 JVM 而言,他们是相互独立的,除了存在同一个包下。这种处理看似没问题,但是当二者之间如果出现相互访问 private 方法的情况就会奔溃。
class Outer { private String name;
class Inner { String sayHi() { return "Hi, " + name + "!"; } }}
复制代码
当被生成兄弟类时,Outer$Inner.sayHi() 无法访问私有的 Outer.name 。所以为了解决这种情况,Java 编译器增加了 package-private synthetic accessor method 处理器来解决这种情况。
class Outer { private String name;++ String access$000() {+ return name;+ }
class Inner { String sayHi() {- return "Hi, " + name + "!";+ return "Hi, " + access$000() + "!"; }
复制代码
我们编译 Outer 类来看一下:
$ javap -c -p Outer.classclass Outer { private java.lang.String name;
static java.lang.String access$000(Outer); Code: 0: aload_0 1: getfield #1 // Field name:Ljava/lang/String; 4: areturn}
复制代码
从今天的角度来看,这对 JVM 来说至多只是一个小麻烦。不过,对于 Android,这些合成访问器方法增加了 dex 文件中的方法计数,增加 apk 大小,降低类加载和验证的速度,将字段查找转换为方法调用同时也使性能降低。
在 Java 11 中,更新了类文件格式用来引入嵌套的概念来描述这些嵌套关系。
$ java -versionjava version "11.0.1" 2018-10-16 LTSJava(TM) SE Runtime Environment 18.9 (build 11.0.1+13-LTS)Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.1+13-LTS, mixed mode)
$ javac *.java
$ javap -v -p *.classclass Outer { private java.lang.String name;}NestMembers: Outer$Inner
class Outer$Inner { final Outer this$0;
Outer$Inner(Outer); Code: …
java.lang.String sayHi(); Code: …}NestHost: class Outer
复制代码
上面的输出被裁剪了,但是我们也能看到这里生成了两个类文件:Outer 和 Outer$Inner。这里的不同是在于 Outer$Inner 作为一个 Out 的成员以及 Outer$Inner 持有 Out 的一个引用,所以 Outer$Inner 可以直接访问外部成员。
遗憾的是 ART 现在还无法解析这种操作。
$ java -jar d8.jar \ --lib $ANDROID_HOME/platforms/android-28/android.jar \ --release \ --output . \ *.classCompilation failed with an internal error.java.lang.UnsupportedOperationException at com.android.tools.r8.org.objectweb.asm.ClassVisitor.visitNestHostExperimental(ClassVisitor.java:158) at com.android.tools.r8.org.objectweb.asm.ClassReader.accept(ClassReader.java:541) at com.android.tools.r8.org.objectweb.asm.ClassReader.accept(ClassReader.java:391) at com.android.tools.r8.graph.JarClassFileReader.read(JarClassFileReader.java:107) at com.android.tools.r8.dex.ApplicationReader$ClassReader.lambda$readClassSources$1(ApplicationReader.java:231) at java.base/java.util.concurrent.ForkJoinTask$AdaptedCallable.exec(ForkJoinTask.java:1448) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020) at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)
复制代码
非常遗憾,在我写这篇文章的时候还是没有支持。但是 ASM(一个字节码操作库) 已经支持了这种 nestmates(嵌套访问)。D8 还没有支持,你可以在 star the D8 feature request 上提出你对这个特性的 issue。
由于 D8 还没有实现 nestmates(嵌套访问),所以在 Android 上还无法使用 Java 11。
4. Java 12
随着 2019 年 3 月发布日期的到来,Java 12 离我们越来越近。这个版本的语言特性和 API 已经开发了几个月。通过早期的访问构建,我们今天可以下载并试用这些特性。
在当前的 EA 构建(编号20)中,有两个新的语言功能可用:expression switch(switch 表达式)和 string literals(字符串文本)。
class Java12 { static int letterCount(String s) { return switch (s) { case "one", "two" -> 3; case "three" -> 5; default -> s.length(); }; }
public static void main(String... args) { System.out.println(` __ ______ ______ ______ ______ ______ ______ /\ \ /\ ___\ /\__ _\ /\__ _\ /\ ___\ /\ == \ /\ ___\ \ \ \____ \ \ __\ \/_/\ \/ \/_/\ \/ \ \ __\ \ \ __< \ \___ \ \ \_____\ \ \_____\ \ \_\ \ \_\ \ \_____\ \ \_\ \_\ \/\_____\ \/_____/ \/_____/ \/_/ \/_/ \/_____/ \/_/ /_/ \/_____/`); System.out.println("three: " + letterCount("three")); }}
复制代码
这两个特性完全实现为 Java 编译器的一部分,而没有任何新的字节码或 API。
$ java -versionopenjdk version "12-ea" 2019-03-19OpenJDK Runtime Environment (build 12-ea+20)OpenJDK 64-Bit Server VM (build 12-ea+20, mixed mode, sharing)
$ javac *.java
$ java -jar d8.jar \ --lib $ANDROID_HOME/platforms/android-28/android.jar \ --release \ --output . \ *.class
$ lsJava12.java Java12.class classes.dex
复制代码
我们可以把这个类打包放到设备中运行。
$ adb push classes.dex /sdcardclasses.dex: 1 file pushed. 0.6 MB/s (1792 bytes in 0.003s)
$ adb shell dalvikvm -cp /sdcard/classes.dex Java12
__ ______ ______ ______ ______ ______ ______/\ \ /\ ___\ /\__ _\ /\__ _\ /\ ___\ /\ == \ /\ ___\\ \ \____ \ \ __\ \/_/\ \/ \/_/\ \/ \ \ __\ \ \ __< \ \___ \ \ \_____\ \ \_____\ \ \_\ \ \_\ \ \_____\ \ \_\ \_\ \/\_____\ \/_____/ \/_____/ \/_/ \/_/ \/_____/ \/_/ /_/ \/_____/
three: 5
复制代码
switch 表达式编写的返回语句,以及字符串中直接使用换行(以前可以使用 \n 转义字符)实现。
与其他所有版本一样,Java 12 中也有新的 API,同样如果需要添加到 Android SDK 中,需要使用脱糖进行。
希望在 Java 12 真正发布的时候,D8 将实现 Java 11 的 nestmates(嵌套访问)。否则,被困在 Java 10 上的痛苦将会增加很多!
5. 总结
随着 Java 生态系统向前迈进新的版本,Java 8 语言特在使用过程遇到的问题正在逐渐变少,令人放心的是,Java 8 和 Java 12 两个版本之间的所有语言特征已经在 Android 上可用。
尽管如此,最终建议仍与上一篇文章中的建议相同。Android 对于新版本 Java 的 API 和 VM 特性的支持非常重要。如果没有将新 API 集成到 SDK 中,它们就不能(很容易)通过脱糖来使用。如果没有将虚拟机功能集成到 ART D8 中,那么对于所有 API 级别来说都将承担一个巨大的负担,而不仅仅是提供向后兼容性。
下一篇文章将介绍 D8 如何解决 VM 中特定版本和特定供应商引起的 bug。
评论