巨佬Jake Wharton谈Android对Java 8的支持

public static void main(String… args) {

sayHi(s -> System.out.println(s));

}

private static void sayHi(Logger logger) {

logger.log(“Hello!”);

}

}

通过 javac 指令编译为字节码后,然后通过 dx 工具编译打包为 dex 文件,但是出错了。

$ 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 使用了 invokedynamic 字节码指令,invokedynamic 是在 Java 7 中引入的。上面的错误信息提示,Android 支持这种字节码的最低版本是 26。与此同时 Android 使用 desugaring(脱糖)兼容所有 API 版本上使用 lambda 表达式。

###Desugaring(脱糖)的历史

脱糖工具的发展史非常出彩,但是它的核心目标却是一致的:让所有的 Java 语言新特性都能运行在所有设备上。

Retrolambda 是最初支持 lambda 表达式的第三方工具库,它通过在编译时利用 JVM 指令将 lambda 转换为内部类来实现。然而生成的类会使方法数激增,但是随着时间的推移,使用该工具的成本降低到了合理的水平。

然后,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

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

当然我也为你们整理好了百度、阿里、腾讯、字节跳动等等互联网超级大厂的历年面试真题集锦。这也是我这些年来养成的习惯,一定要学会把好的东西,归纳整理,然后系统的消化吸收,这样才能极大的提高学习效率和成长进阶。碎片、零散化的东西,我觉得最没有价值的。就好比你给我一张扑克牌,我只会觉得它是一张废纸,但如果你给我一副扑克牌,它便有了它的价值。这和我们收集资料就要收集那些系统化的,是一个道理。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

=“https://i-blog.csdnimg.cn/blog_migrate/39c26b63d35ebded53c02b75554ed9ed.jpeg” />

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

当然我也为你们整理好了百度、阿里、腾讯、字节跳动等等互联网超级大厂的历年面试真题集锦。这也是我这些年来养成的习惯,一定要学会把好的东西,归纳整理,然后系统的消化吸收,这样才能极大的提高学习效率和成长进阶。碎片、零散化的东西,我觉得最没有价值的。就好比你给我一张扑克牌,我只会觉得它是一张废纸,但如果你给我一副扑克牌,它便有了它的价值。这和我们收集资料就要收集那些系统化的,是一个道理。

[外链图片转存中…(img-jkeGk84j-1711960854706)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 13
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值