本文原文出自 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
$ ls
Java9TryWithResources.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 *.class
class 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
$ ls
Java9AnonymousDiamond.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
$ ls
Java9PrivateInterface.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.dex
Class #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 -version
java 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 *.class
class 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 -version
java 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 *.class
class 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>:()V
0005: 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 v2
0019: 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 *.class
Compiled 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 -version
java version "10" 2018-03-20
Java(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
$ ls
Outer.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.class
class 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 -version
java version "11.0.1" 2018-10-16 LTS
Java(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 *.class
class 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 . \
*.class
Compilation 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 -version
openjdk version "12-ea" 2019-03-19
OpenJDK 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
$ ls
Java12.java Java12.class classes.dex
复制代码
我们可以把这个类打包放到设备中运行。
$ adb push classes.dex /sdcard
classes.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
。
评论