然后,Android 工具团队宣布了一个新的编译器,它将提供 Java 8 语言特性的支持,以及更好的性能。该工具是建立在 Eclipse Java 编译器上的,而不是 Dalvik Java 字节码之上的。虽然处理 Java 8 效率很高,但是它的体验很差以及无法与别的工具兼容。
最终新的编译器被舍弃,同时在 Android Gradle plugin 中引入了谷歌定制的字节码构建系统,因为脱糖是增量式的,所以脱糖的输出效率仍然不是很理想,与此同时,正在进行的工作有了更好的方案。
D8 编译工具问世了。D8 编译工具用来替代老的
dx 工具,同时在 D8 中集成了脱糖,以此取代脱糖作为一个独立的字节码转换模块的方式。D8 相比较 dx 有很大的提升,带来了更有效率的字节码转换。同时在 Android Gradle Plugin 3.1 中作为默认 dex 编译器,然后在 3.2 版本中 D8 又集成了脱糖。
###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
同时我们可以通过 Android 提供的 dexdump 工具来查看 dex 文件内容,看看 D8 是如何脱糖的,由于 dexdump 会产生很多代码,我们只截取一部分。
$ $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
…
在 main 方法中,对应 0000 位置创建了一个 Java8$1 类对象 INSTANCE 实例,但是我们的源文件中并不包含这个类,所以猜测这个类是由脱糖产生的。同时 main 方法的字节码中也没有包含任何 lambda 的实现,所以很可能是在 Java8 1 中 实 现 的 。 在 0002 位 置 , I N S T A N C E 调 用 了 s a y H i 方 法 , 同 时 可 以 看 到 s a y H i 方 法 的 参 数 是 L J a v a 8 1 中实现的。在 0002 位置,INSTANCE 调用了 sayHi 方法,同时可以看到 sayHi 方法的参数是 LJava8 1中实现的。在0002位置,INSTANCE调用了sayHi方法,同时可以看到sayHi方法的参数是LJava8Logger ,所以基本可以确定 Java8$1 类实现了 lambda 中的接口。我们可以输出字节码进行验证。
Class #2 -
Class descriptor : ‘LJava8$1;’
Access flags : 0x1011 (PUBLIC FINAL SYNTHETIC)
Superclass : ‘Ljava/lang/Object;’
Interfaces -
#0 : ‘LJava8$Logger;’
SYNTHETIC 字节码标签代表着这个类是由系统产生,通过 Interfaces 可以看到 LJava8 1 类 实 现 了 L J a v a 8 1 类实现了 LJava8 1类实现了LJava8Logger 接口。
现在 LJava8$1 的实现已经替代了 lambda,我们可以通过查看 sayHi 方法的字节码实现。
…
[00026c] Java8$1.log:(Ljava/lang/String;)V
0000: invoke-static {v1}, LJava8;.lambda$main$0:(Ljava/lang/String;)V
0003: return-void
…
在 sayHi 的字节码实现中,它调用了 Java8 类中的静态方法 lambda$main$0,但是我们并没有在类中定义这个方法,所以我们只能查看下 Java8 类对应的字节码。
…
#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$main$0 方法是由系统自动生成的,并且看到了 lambda 实现的方法体 System.out.println。
通过上面的流程分析,我们可以推测出:lambda 的实现保持在原来的主类中,并且是私有的,别的类无法直接访问。
Source Transformation(源码模拟实现)
为了更好的理解脱糖是如何工作的,我们可以在源码的层面模拟实现,注意这里的模拟仅仅是为了加深理解,脱糖实际工作比这个要复杂的多。
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 表达式移到同级的包私有方法。
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);
-
}
+}
最后,因为 lambda 方法并没有依赖外部的任何类,所以我们在 Java8$1 内部创建一个单例对象来避免每次调用 lambda 方法都生成一个新对象。
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) {
最终我们经过脱糖生成的文件适用与所有 APIs 。
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);
}
}
实际上你在查看 lambda 表达式生成的 Dalvik 字节码时可能看到不是类似 Java8 1 的 名 称 , 而 是 像 这 样 的 − 1 的名称,而是像这样的 - 1的名称,而是像这样的− L a m b d a Lambda LambdaJava8$QkyWJ8jlAksLjYziID4cZLvHwoY 名称,这是由于命名规范不恰当引起的。
###Native Lambdas
在上面我们通过 dx 工具编译 dex 文件时,错误信息提示我们最低的支持版本是 API 26。
$ $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
所以如果我们在使用 D8 的时候指定 --min-api 26 版本,应该就不会报错了。
$ java -jar d8.jar \
–lib $ANDROID_HOME/platforms/android-28/android.jar \
–release \
–min-api 26 \
–output . \
*.class
同样为了查看 D8 如何工作,我们还是查看 Java8 类的字节码。
$ javap -v Java8.class
class Java8 {
public static void main(java.lang.String…);
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:log:()LJava8$Logger;
5: invokestatic #3 // Method sayHi:(LJava8$Logger;)V
8: return
}
…
为了阅读方便我只截取了部分代码,但是我们同样可以在 main 方法中看到这里使用了 InvokeDynamic 指令,在 Code 表的 0 位置上,我们可以看到第二个参数是 0,对应着 bootstrap method(引导方法)。bootstrap method(引导方法)是当字节码第一次执行时首先被执行的一小段代码。
…
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 method(引导方法)对应的是 java.lang.invoke.LambdaMetafactory 类中的 metafactory 方法。LambdaMetafactory 类在运行时为 lambda 表达式生成匿名类,而 D8 是在编译时生成。
如果我们查看 Android documentation for java.lang.invoke 和 AOSP source code for java.lang.invoke 的文档,我们可以注意到这个类在 Android Runtime 中不存在,这也是为什么脱糖在编译时要求最小版本的原因。VM 环境支持 invokedynamic 指令,但是 JDK 在编译 LambdaMetafactory 中却不可用。
Method References(方法引用)
除了 lambda 表达式,方法引用也是 Java 8 的语言特性,当 lambda 的实现是一个已经存在的方法,此时使用方法引用会很方便。
public static void main(String… args) {
- sayHi(s -> System.out.println(s));
- sayHi(System.out::println);
}
这与 javac 和 dexes 与 D8 的编译是相同的,与 lambda 版本有一个显著的区别。在编译为 dalvik 字节码时,生成的 lambda 类的主体已更改。
[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 表达式调用类也不是一个静态单例,而是直接使用 PrintStream 类实例引用,即 System.out,它的调用如下。
[0002bc] Java8.main:([Ljava/lang/String;)V
0000: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;
0003: new-instance v0, L-$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM;
0004: invoke-direct {v0, v1}, L-$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM;.:(Ljava/io/PrintStream;)V
0008: invoke-static {v0}, LJava8;.sayHi:(LJava8$Logger;)V
同样我们也可以在源码级层面进行模拟。
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);
-
}
+}
###Interface Methods(接口中的方法)
在 Java 8 中新增了接口方法中的 default 和 static 修饰符。接口中的 static 方法允许直接操作调用。接口中的 default 方法允许你为接口添加默认实现方法。
interface Logger {
void log(String s);
default void log(String tag, String s) {
log(tag + ": " + s);
}
static Logger systemOut() {
return System.out::println;
}
}
D8 中的脱糖都已经支持了这两个接口的新特性。通过上面的方法同样可以分析出脱糖是如何进行优化工作的,具体的分析就留给读者了。
###Just Use Kotlin?
这个时候肯定有很多读者猜想 Kotlin 是否也具备这种能力。当然,Kotlin 同样提供了 lambda 和接口中的 static 和 default 方法。这些特性都被 kotlinc 以相同的方式实现。
Android 工具和 VM 的开发者肯定会 100% 支持 Kotlin 实现 Java 语言的新特性。因为每次的 Java 新版本都会在字节码构建和 VM 上带来新的优化体验。
在未来很可能 Kotlin 不会支持 Java 6 和 Java 7,Intellij 开发工具已经在在 2016 年 1 月迁移至 Java 8。
###Desugaring APIs
上面的分析中,我们一直关注的是 Java 语言新特性,其它还有一些主要的方面没有提及,比如新的 APIs。在 Java 8 转给你带来了很多新的 APIs,比如 stream、Optional、CompletableFuture 以及新的 date/time API 等等。 回到上面的例子,我们使用新的 date/time 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 指令和 d8 指令进行编译:
$ javac *.java
$ java -jar d8.jar \
–lib $ANDROID_HOME/platforms/android-28/android.jar \
–release \
–output . \
*.class
当编译完成后,我们可以将它运行在一个手机或模拟器中。
最后
小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
如果你需要这些资料, ⬅ 专栏获取
我们同样使用 javac 指令和 d8 指令进行编译:
$ javac *.java
$ java -jar d8.jar \
–lib $ANDROID_HOME/platforms/android-28/android.jar \
–release \
–output . \
*.class
当编译完成后,我们可以将它运行在一个手机或模拟器中。
最后
小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
[外链图片转存中…(img-0OLn2zss-1719455958253)]
[外链图片转存中…(img-0PZfTTmM-1719455958254)]
[外链图片转存中…(img-z5EngMUH-1719455958254)]
[外链图片转存中…(img-Fok4BbSc-1719455958255)]
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
如果你需要这些资料, ⬅ 专栏获取