哈Ha! 我提请您注意,
臭名昭著的杰克·沃顿(Jake Worton)撰写的一系列精彩文章的翻译,内容涉及Java如何支持Android 8。
原始文章在这里我在家工作了几年,经常听到同事抱怨Android支持不同版本的Java。
这是一个相当复杂的话题。 首先,您需要确定“ Android中的Java支持”的含义,因为在该语言的一个版本中,可能有很多东西:功能(例如,lambda),字节码,工具,API,JVM等。
当他们谈论Android中对Java 8的支持时,通常是指语言功能的支持。 因此,让我们从它们开始。
Lambdas
lambda是Java 8的主要创新之一。
代码变得更加简洁明了,lambda使我们不再需要使用带有单个方法的接口编写庞大的匿名类。
class Java8 { interface Logger { void log(String s); } public static void main(String... args) { sayHi(s -> System.out.println(s)); } private static void sayHi(Logger logger) { logger.log("Hello!"); } }
使用javac和legacy
dx tool
对此进行编译后,会出现以下错误:
$ javac *.java $ ls Java8.java Java8.class Java8$Logger.class $ $ANDROID_HOME/build-tools/28.0.2/dx --dex --output . *.class Uncaught translation error: com.android.dx.cf.code.SimException: ERROR in Java8.main:([Ljava/lang/String;)V: invalid opcode ba - invokedynamic requires --min-sdk-version >= 26 (currently 13) 1 error; aborting
发生此错误是由于lambda使用Java 7中添加的字节码
invokedynamic
的新指令这一事实。从错误文本中,您可以看到Android仅从26 API(Android 8)开始支持它。
听起来不太好,因为几乎没有人会发布具有26 minApi的应用程序。 为了解决这个问题,使用了所谓的
desugaring流程,该流程使lambda支持在所有版本的API上都可以实现。
糖化历史
她在Android世界中非常丰富多彩。 脱糖的目标始终是相同的-允许新的语言功能在所有设备上都能使用。
最初,例如,为了在Android中支持lambda,开发人员连接了
Retrolambda插件。 他使用了与JVM相同的内置机制,将lambda转换为类,但是他是在运行时而不是在编译时执行的。 就方法的数量而言,生成的类非常昂贵,但是随着时间的流逝,经过不断的改进和改进,该指标或多或少地变得合理。
然后,Android团队
宣布了一个支持所有Java 8功能且生产率更高
的新编译器 。 它建立在Eclipse Java编译器的基础上,但是没有生成Java字节码,而是生成了Dalvik字节码。 但是,其性能仍然有很多不足之处。
幸运的是,当新的编译器被放弃时,负责处理的Java字节码中的Java字节码转换器已从Google的构建系统
Bazel 集成到Android Gradle插件中 。 而且它的性能仍然很低,因此并行寻求更好的解决方案。
现在我们看到
dexer
-
D8 ,它应该可以代替
dx tool
。 现在,在将已编译的JAR文件转换为
.dex
(dexing)的过程中执行了脱糖操作。 与
dx
相比,D8的性能要好得多,并且由于Android Gradle Plugin 3.1,它已成为默认的dexer。
D8
现在,使用D8,我们可以编译上面的代码。
$ java -jar d8.jar \ --lib $ANDROID_HOME/platforms/android-28/android.jar \ --release \ --output . \ *.class $ ls Java8.java Java8.class Java8$Logger.class classes.dex
要查看D8如何转换lambda,您可以使用Android SDK中随附的
dexdump tool
。 它会显示很多东西,但是我们将只关注于此:
$ $ANDROID_HOME/build-tools/28.0.2/dexdump -d classes.dex [0002d8] Java8.main:([Ljava/lang/String;)V 0000: sget-object v0, LJava8$1;.INSTANCE:LJava8$1; 0002: invoke-static {v0}, LJava8;.sayHi:(LJava8$Logger;)V 0005: return-void [0002a8] Java8.sayHi:(LJava8$Logger;)V 0000: const-string v0, "Hello" 0002: invoke-interface {v1, v0}, LJava8$Logger;.log:(Ljava/lang/String;)V 0005: return-void …
如果您还没有阅读字节码,请不要担心:这里写的大部分内容都可以直观地理解。
在第一个块中,索引为
0000
main
方法从
INSTANCE
字段获取对
INSTANCE
类
Java8$1
的引用。 该类是在
过程中生成的。 main方法字节码也没有包含我们的lambda的任何内容,因此,很可能与
Java8$1
类相关联。 索引
0002
然后使用到
INSTANCE
的链接调用
sayHi
静态方法。
sayHi
需要
Java8$Logger
,因此似乎
Java8$1
实现了此接口。 我们可以在这里验证:
Class #2 - Class descriptor : 'LJava8$1;' Access flags : 0x1011 (PUBLIC FINAL SYNTHETIC) Superclass : 'Ljava/lang/Object;' Interfaces - #0 : 'LJava8$Logger;'
SYNTHETIC
标志意味着已经生成
Java8$1
类,并且它包含的接口列表包含
Java8$Logger
。
此类表示我们的lambda。 如果查看
log
方法的实现,您将看不到lambda的主体。
… [00026c] Java8$1.log:(Ljava/lang/String;)V 0000: invoke-static {v1}, LJava8;.lambda$main$0:(Ljava/lang/String;)V 0003: return-void …
相反,
Java8
类的
static
方法
Java8
lambda$main$0
。 我重复一遍,此方法仅以字节码表示。
… #1 : (in LJava8;) name : 'lambda$main$0' type : '(Ljava/lang/String;)V' access : 0x1008 (STATIC SYNTHETIC) [0002a0] Java8.lambda$main$0:(Ljava/lang/String;)V 0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; 0002: invoke-virtual {v0, v1}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V 0005: return-void
SYNTHETIC
标志再次告诉我们该方法已生成,其字节码仅包含lambda主体:对
System.out.println
的调用。 lambda主体位于
Java8.class内的
原因很简单-它可能需要访问该类的
private
成员,生成的类将无法访问该
private
成员。
上面介绍了了
解脱糖工作原理所需的所有内容。 但是,在Dalvik字节码中查看它,您会发现一切都变得更加复杂和令人恐惧。
源转换
为了更好地了解如何进行
糖化 ,让我们逐步尝试将类转换为可在所有版本的API上使用的类。
让我们以lambda为基础来讨论同一类:
class Java8 { interface Logger { void log(String s); } public static void main(String... args) { sayHi(s -> System.out.println(s)); } private static void sayHi(Logger logger) { logger.log("Hello!"); } }
首先,将lambda主体移至
package private
方法。
public static void main(String... args) { - sayHi(s -> System.out.println(s)); + sayHi(s -> lambda$main$0(s)); } + + static void lambda$main$0(String s) { + System.out.println(s); + }
然后,实现一个实现
Logger
接口的类,在其中执行来自lambda主体的代码块。
public static void main(String... args) { - sayHi(s -> lambda$main$0(s)); + sayHi(new Java8$1()); } @@ } + +class Java8$1 implements Java8.Logger { + @Override public void log(String s) { + Java8.lambda$main$0(s); + } +}
接下来,
Java8$1
的单例实例,该实例存储在
static
变量
INSTANCE
。
public static void main(String... args) { - sayHi(new Java8$1()); + sayHi(Java8$1.INSTANCE); } @@ class Java8$1 implements Java8.Logger { + static final Java8$1 INSTANCE = new Java8$1(); + @Override public void log(String s) {
这是可以在所有版本的API上使用的最终
配音类:
class Java8 { interface Logger { void log(String s); } public static void main(String... args) { sayHi(Java8$1.INSTANCE); } static void lambda$main$0(String s) { System.out.println(s); } private static void sayHi(Logger logger) { logger.log("Hello!"); } } class Java8$1 implements Java8.Logger { static final Java8$1 INSTANCE = new Java8$1(); @Override public void log(String s) { Java8.lambda$main$0(s); } }
如果您在Dalvik字节码中查看生成的类,将不会找到类似Java8 $ 1的名称-会有类似
-$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY
。 为该类生成此类命名的原因及其优点是吸引到单独的文章中。
本机Lambda支持
当我们使用
dx tool
编译包含lambda的类时,出现一条错误消息,指出仅适用于26个API。
$ $ANDROID_HOME/build-tools/28.0.2/dx --dex --output . *.class Uncaught translation error: com.android.dx.cf.code.SimException: ERROR in Java8.main:([Ljava/lang/String;)V: invalid opcode ba - invokedynamic requires --min-sdk-version >= 26 (currently 13) 1 error; aborting
因此,如果我们尝试使用
—min-api 26
标志进行编译,这似乎是合乎逻辑的,则不会发生脱糖作用。
$ java -jar d8.jar \ --lib $ANDROID_HOME/platforms/android-28/android.jar \ --release \ --min-api 26 \ --output . \ *.class
但是,如果转储
.dex
文件,则仍可以在其中找到它
-$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY
。 为什么这样 这是D8错误吗?
为了回答这个问题,以及为什么
总是发生脱糖
作用 ,我们需要查看
Java8
类的Java字节码。
$ javap -v Java8.class class Java8 { public static void main(java.lang.String...); Code: 0: invokedynamic
在
main
方法内部,我们再次在索引
0
处看到
invokedynamic 。 调用中的第二个参数为
0
与之关联的
引导方法的索引。
这是
引导方法的列表:
… BootstrapMethods: 0: #27 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:( Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String; Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType; Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;) Ljava/lang/invoke/CallSite; Method arguments: #28 (Ljava/lang/String;)V #29 invokestatic Java8.lambda$main$0:(Ljava/lang/String;)V #28 (Ljava/lang/String;)V
这里的
bootstrap方法在
java.lang.invoke.LambdaMetafactory
类中称为
metafactory
。 他
住在JDK中,并在运行时为lambda创建匿名的即时类,就像D8在计算时生成它们一样。
如果您查看
Android java.lang.invoke
的
Android java.lang.invoke
或
AOSP java.lang.invoke
的
AOSP java.lang.invoke
,我们看到此类不在运行时中。 这就是为什么无论您拥有什么minApi,都总是在编译时进行去杂耍。 VM支持类似于
invokedynamic
字节码指令,但是JDK内置的
invokedynamic
无法使用。
方法参考
Java 8与lambda一起添加了方法引用-这是创建其lambda主体引用现有方法的有效方法。
我们的
Logger
界面就是这样一个例子。 lambda主体引用到
System.out.println
。 让我们将lambda转换为引用方法:
public static void main(String... args) { - sayHi(s -> System.out.println(s)); + sayHi(System.out::println); }
当我们编译它并查看字节码时,我们将看到与先前版本的不同之处:
[000268] -$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM.log:(Ljava/lang/String;)V 0000: iget-object v0, v1, L-$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM;.f$0:Ljava/io/PrintStream; 0002: invoke-virtual {v0, v2}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V 0005: return-void
而不是调用生成的
Java8.lambda$main$0
(其中包含对
System.out.println
的调用),现在直接调用
System.out.println
。
具有lambda的类不再是
static
单例,而是通过字节码中的索引
0000
,我们看到我们获得了指向
PrintStream
-
System.out
的链接,该链接随后用于对其调用
println
。
结果,我们的班级变成了这样:
public static void main(String... args) { - sayHi(System.out::println); + sayHi(new -$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM(System.out)); } @@ } + +class -$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM implements Java8.Logger { + private final PrintStream ps; + + -$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM(PrintStream ps) { + this.ps = ps; + } + + @Override public void log(String s) { + ps.println(s); + } +}
接口中的Default
方法和static
方法
Java 8带来的另一个重要而重要的变化是能够在接口中声明
default
方法和
static
方法。
interface Logger { void log(String s); default void log(String tag, String s) { log(tag + ": " + s); } static Logger systemOut() { return System.out::println; } }
D8也支持所有这些。 使用与以前相同的工具,很容易看到具有
default
方法和
static
方法的Logger的登录版本。 lambda和
method references
的区别之一是,默认方法和静态方法是在Android VM中实现的,并且从24个API开始,D8不会
将它们
解耦 。
也许只用Kotlin?
阅读本文时,大多数人可能会想到Kotlin。 是的,它支持所有Java 8功能,但是除了一些细节外,它们由
kotlinc
以与D8相同的方式实现。
因此,即使您的项目是100%用Kotlin编写的,Android对Java新版本的支持仍然非常重要。
将来Kotlin可能会停止支持Java 6和Java 7字节码,
IntelliJ IDEA将从Gradle 5.0切换到Java8。运行在较早JVM上的平台数量正在减少。
脱糖API
一直以来,我都在谈论Java 8功能,但没有对新的API进行任何说明-流,
CompletableFuture
,日期/时间等。
回到Logger示例,我们可以使用新的日期/时间API来查找发送消息的时间。
import java.time.*; class Java8 { interface Logger { void log(LocalDateTime time, String s); } public static void main(String... args) { sayHi((time, s) -> System.out.println(time + " " + s)); } private static void sayHi(Logger logger) { logger.log(LocalDateTime.now(), "Hello!"); } }
再次使用
javac
对其进行
javac
并使用D8将其转换为Dalvik字节码,从而
将其
解耦以支持所有版本的API。
$ javac *.java $ java -jar d8.jar \ --lib $ANDROID_HOME/platforms/android-28/android.jar \ --release \ --output . \ *.class
您甚至可以在设备上运行此程序以确保其正常运行。
$ adb push classes.dex /sdcard classes.dex: 1 file pushed. 0.5 MB/s (1620 bytes in 0.003s) $ adb shell dalvikvm -cp /sdcard/classes.dex Java8 2018-11-19T21:38:23.761 Hello
如果此设备上使用API 26及更高版本,则会显示Hello消息。 如果没有,我们将看到以下内容:
java.lang.NoClassDefFoundError: Failed resolution of: Ljava/time/LocalDateTime; at Java8.sayHi(Java8.java:13) at Java8.main(Java8.java:9)
D8处理了lambdas(一种参考方法),但与
LocalDateTime
无关,这很可悲。
开发人员必须在日期/时间api上使用自己的实现或包装器,或者使用
ThreeTenBP
类的
ThreeTenBP
来处理时间,但是为什么不能用自己的双手做D8?
结语
缺少对所有新Java 8 API的支持仍然是Android生态系统中的一个大问题。 毕竟,我们每个人不太可能允许我们在项目中指定26分钟的API。 同时支持Android和JVM的库无法使用5年前引入的API!
即使现在Java 8支持已成为D8的一部分,每个开发人员仍应在Java 8中明确指定源和目标兼容性。如果编写自己的库,则可以通过布局使用Java 8字节码的库来加强这种趋势。 (即使您没有使用新的语言功能)。
在D8上正在进行许多工作,因此在支持语言功能的情况下,似乎以后一切都会好起来的。 即使您仅在Kotlin上编写代码,强制Android开发团队支持所有新版本的Java,改进字节码和新的API也非常重要。
这篇文章是我的演讲“
深入研究D8和R8”的书面版本。