安卓10兼容安卓8_ Android兼容Java 8语法特性的原理分析

本文阐述了Lambda方程及其基本实现( invokedynamic指令)原理、Android第三方插件RetroLambda支持过程以及Android公式最新的dex编译器D8编译支持。 通过这三个方面的跟踪分析,我们着眼于Java 8的代表性特性——Lambda方式,共享Android如何与Java 8兼容。

Java 8概述

Java 8是Java开发语言的重要版本。 Oracle于2014年3月18日发布了Java 8,从此发布开始,Java支持函数编程。 特别是通过吸收JVM上执行的Scala、Groovy等动态脚本语言的特性,Java 8在语言的表现力、简洁性两方面都有了很大的提高。

概括Java 8的主要语言特性改进包括

Lambda表示(函数闭包)函数表示式介面( @FunctionalInterface)Stream API (在流程呼叫中支援高阶函数,例如map、filter )方法参照(将函数以::关键字转换为物件) 缺省方法(抽象接口允许default修饰的非抽象方法)类型的注释和重复注释中,Lambda表示法、函数表达式接口和方法引用这三个特性给Java带来了函数表达式编程样式Stream是map,而 实现了reduce等常见的高次函数,数据源包括数组、集合、IO通道等,它们给Java带来了流式编程和链式编程的样式,这些样式使Java越来越现代化和易于使用。

Android与Java的关系

事实上,Java在Android的快速发展过程中发挥着非常重要的作用,也是开发语言( Java )、开发框架( Android-SDK引用了80%的JDK-API )、开发工具( Eclipse or Android Studio )。 这些都和Java有几千条关系。 但是,Google可能受到Oracle诉讼的影响,因此在Android上不积极升级Java

Android已从1.0升级到4.4,Android版本重复了近19个,版本4.4支持Java 7。 从Android4. 4版到Android7. 0版,Jack/Jill工具链支持Java 8。 但是,由于Jack/Jill工具链在构建过程中丢弃了旧的Java字节码体系,许多现有技术的沉淀无法应用,许多App项目放弃了访问。 最后,直到Android P(9.0 )版本,Google才正式支持Android Studio 3.x中新增的D8 dex编译器中的Java 8,但有些API不完全支持。 他说“有了几次奇怪的经历”。 特别是在Rx流行的今天,Rx与Java 8的特性Lambda合作,带来了简洁高效的开发体验,让android开发人员欣喜若狂。

然后从技术原理的层面分析Android如何支持Java 8。

Lambda公式

为了更好地理解Android支持Java 8的过程,Lambda表达式这个代表性的“语法糖”是非常好的起点。 首先,我们需要知道Lambda表达式是什么。它的基本实现原理是什么?

Lambda方案是Java支持函数编程的基础,也称为闭包。 简而言之,可以在Java语法级别上将函数作为方法的参数来处理,并将函数作为对象来处理。 从任何Lambda方式都只对应一个函数式接口的观点来看,也可以说是该函数式接口的实例化。

Lambda公式

通用格式:

简单示例:

说明:

Lambda表达式中的() 对应于函数类型接口-run方法的参数列表。 Lambda表达式中的system.out.println ( ' ' ' Xixi ' '/system.out.println ( ' ' ' haha ' )是运行时特定run方法的实现。 Lambda公式原理

让我们看一下实例中的代码的编译字节代码

javacj8sample.Java-> j8sample.class javap-c-jpj8sample.class

字节码包括以下内容

实例的Lambda表达式1为字节码块Line 11的0:invokedynamic # 2,0// invokedynamic #0: run:( ) ljava/lang/runnable。 实例的Lambda表达式2为字节码块的Line 20的21:invokedynamic # 6,0// invokedynamic #1: run:( ) ljava/lang/runnable。 您可以看到,Lambda表达式是在虚拟机级别通过名为invokedynamic的字节代码指令实现的。 invokedynamic在哪里是神圣的?

invokedynamic命令解密

invokedynamic命令是Java 7中新添加的字节码调用命令,作为Java支持动态类型语言的改进之一,有四个命令: invokevirtual、invokestatic、invokeinterface和invokespecial 不同点是

后者的四个指令是在编译时生成的类文件,通过常量池( Constant Pool )中的MethodRef常量来固定目标方法的符号信息(方法的所有者及其类型、方法名称、参数顺序和类型、返回值)。 虚拟机可以使用符号信息直接说明具体方法,直接调用。 invokedynamic命令是在编译时生成的class文件中,在与常量池( Constant Pool )对应的Invokedynamic_Info常量中存储的符号信息中没有方法的所有者及其类型,取而代之的是BootstapMethod信息 在运行时,方法的所有者和类型由启动方法的BootstrapMethod机制动态确定。 该特征也同样适用于动态类型的语言只能在运行时确定类型的特征。 那么,invokedynamic如何以引导方式找到所有者及其类型? 让我们结合上一个J8Sample实例

javap -v J8Sample.class

结合J8Sample.class字节码,跟踪分析invokedynamic指令调用过程。 总结如下:

基于上图的invokedynamic呼叫程序,我们进一步进行了分析说明。

步骤j 8为J8Sample.java源选择Lambda表达式1

runnable runnable = ( )-> system.out.println ( ' ' ' Xixi ' ) : // lambda式1

程序使用javac J8Sample.java编译得到J8Sample.class后,Lambda方程式1为:0: invokedynamic # 2,0// invokedynamic #0: run:( ) ljava/lang/runnable; 在相应的J8Sample.class中找到新的专用静态方法:

步骤3表达式1中的字节码分析#2对应于class文件中的常量池

#2= invokedynamic #0: # 35// #0: run:( ) ljava/lang/runnable;

请注意,InvokeDynamic代表Constant_InvokeDynamic_Info结构,而不是指令。

步骤4结构后面的#0显示了类文件的BootstrapMethod区域中引导方法的索引

步骤5启动方法的Java/lang/invoke/lambdetafactory.meta factory是invokedynamic命令的关键点

此方法在内存中动态生成实现与Lambda表达式对应的函数类型接口的实例类型,并调用在接口实现方法中在步骤2中添加的静态专用方法。

步骤6使用Java-djk.internal.lambda.dupproxyclassesj8sample.class执行时,可以本地输出内存中动态生成的类型

步骤7使用javap-p-CJ8sample $ $ lambda $1. class逆编译可以查看生成类的实现

run方法使用invokestatic命令直接调用J8Sample.lambda$main$0,它是编译时生成的静态私有方法。

迄今为止,上述的7个步骤是在Lambda方式的Java的基础上的实现原理。 Android如何应对这些实现呢?

不能直接支持Android

返回Android系统时,Java-Bytecode(JVM字节码)无法在Android系统中直接运行,因此需要转换为Android-Bytecode(Dalvik/ART字节码)。

图:

在Lambda中,Java的基础是通过invokedynamic指令实现的。 因为Dalvik/ART不支持invokedynamic指令或相应的备用功能。 简而言之,Android的dex编译器不支持invokedynamic命令,Android不能直接支持Java 8。

Android间接支持

由于不能直接支持,因此只能在将Java-Bytecode转换为Android-Bytecode的过程中考虑方法,并间接支持。 这种间接支持过程统称为Desugar (脱糖)过程。

官方流程图:

目前,RetroLambda、Google的Jack & Jill工具或最新的D8 dex编译器:

流程面:均按照上述图所示的公式流程进行设计。 原则上,参考Lambda在Java基础上的实现,将这些实现移动到RetroLambda插件或Jack或D8编译器工具中。 以下,一一分析解读一下吧。

Android间接支持的RetroLambda

如图所示,RetroLambda的Desugar过程发生在javac编译源代码之后,dx工具编译dex之前。

RetroLambda Desugar

使用invokedynamic命令解密第5节中的步骤,并使用Java/lang/invoke/lambdetafactory.meta factory方法在运行时在内存中生成的J8Sample$$Lambda$1.class 使用在dx编译器dex之前本地直接生成的J8Sample$$Lambda$1类修改J8Sample.class字节代码文件,并在J8Sample.class中使用invokedynamic命令作为invokestatic代码

将实例J8Sample.java部署到配置了Retrolambda的Android工程中

编译andoidstudio-> build-> make项目后:

app:tranformatingclasswithrotlambdordebug任务是在app:compileedebugjavawithjavac ( javac )之后执行app:tranformationdexarchiveewithhdexmers 在build/intermediates/transforms/retroblambda下生成图像类文件。

对J8Sample.class和J8Sample$$Lambda$1.class进行反向编译后的代码如下所示

通过对代码进行反编译,可以看出J8Sample.class的Lambda方式被我们熟知的1.7or1.6文件所替换。

注意:右图中的J8Sample.lambda$main$0( )方法未显示在左图中,但实际上存在J8Sample.class字节代码。

Android间接支持的Jack&Jill工具

Jack是在基于Eclipse的ecj编译中开发的,Jill是在基于ASM4的基础上开发的。 Jack&Jill工具链是Google在Android N(7.0)上发表的,它取代了javac&dx的工具链,在jack进程中嵌入了Desugar进程。

但是Android P(9.0)废除了Jack&Jill工具链,取代了javac&D8工具链。 这里不进行Desugar的具体分析。

Android间接支持的D8

D8是Android P(9.0)的新dex编译器。 在androidstudio3. 1版中,缺省情况下使用D8作为dex的缺省编译器。

D8 Desugar

如图所示,Desugar过程位于D8内部,名为Android Studio的IDE实现这一转换基本上是与RetroLambda相同的原理。

基本上,引用Java/lang/invoke/lambdetafactory.meta factory方法,在D8的编译dex之间直接生成运行时在内存中生成的J8Sample$$Lambda$1.class,并将其写入dex文件

同样,将实例J8Sample.java部署到支持D8的Android工程中

同样,在编译andoidstudio-> build-> make project之后:

javac编译的J8Sample.class使用invokedynamic命令。 在这个步骤中没有Desugar

app :在transfortdexarchiveitdexmergeerfordebug (支持dx )任务之后,在build/intermediates/transforms/dex merger目录中找到第0个classex.dex。

运行$ Android _ home/build-tools/28.0.3/dex dump-classes.dex > > dex.txt以获取dex信息。

实例中的Lambda表达式1 :可拆卸调度= ( )-> system.out.println ( ' ' ' Xixi ' ) : 进行分析。

此dexIno.txt文件非常大,有1.4M。 在com.J8Smaple2.J8Sample中找到索引中J8Sample的位置。

添加方法:

J8Sample.main方法:

图中的所选部分对应于Lambda格式1 desugar的内容。

翻译成Java时,会成为newc lcom/j8sample2/-$ $ lambda $ j8sample $ jw mu y0 zef 070 tkxrjbfgnnqokc这一生成类别的对象。 类lcom/j8sample2/-$ $ lambda $ j8sample $ jw mu y0 zef 070 tkxrjbfgnnqokc与以前生成的J8Sample$$Lambda$1类型相对应,但数字1为Hash值。

实现接口ljava/lang/runnable。 lcom/j 8范例2/- $ $ lambda $ j 8范例$ jw mu y0 zef 070 tkxrjbfgnnqokc.run方法:

到目前为止,是否与以前的RetroLambda相同。

总结

Lambda及其invokedynamic指令、RetroLambda插件和D8编译器的原理分析到此结束。

将Lambda在Java8自身内部的实现进行比较,在存储器中动态生成相关函数型接口的实现类型,并且在bsm引导方法中发现存储器类型(字节码级别反射)。

Android上的其他3种Desugar方式原理相同,不同的是时机不同

RetroLambda在编译javac之后、编译dx之前动态地修改了表达式所属的字节码文件,以实现对应于函数型接口的实例型生产过程。 Jack&Jill是直接对应接口的实例类型,由jack进程生成并编译为dex文件。 D8的过程是在编译dex时直接在内存中生成与接口相对应的实例类型,并将生成的类型直接写入生成的dex文件。 研究

RetroLambda和D8都不支持Java8的所有特性。

在Java8中新增的许多API (例如,新的DataAPI )只能在D8编译器的Android P(9.0 )版本中直接运行。 不能在9.0以下。 如何支持完整版本的Java 8。 D8还有很长的路要走。

如果需要在早期版本中使用新的API,则当前可以采用打包这些API的临时方法。

正如这里所写的那样,为什么不直接使用Kotlin呢?确实Kotlin支持Lambda式、函数引用等特性,但实际上很难取代Android的Java。 新业务,新工程还比较容易,对旧业务来说,尤其是经过多年沉淀,工程结构复杂,转移改造带来的收益往往远小于转移改造带来的成本和不可控制的风险。 Kotlin和Java同时存在的情况,从长期来看是必然的结果。

对于Java 8的其他特性,D8是如何实现的,可以如前所述进行分析,也可以结合Kotlin实现的方法,探索一体化。

作者的个人资料

元合、朝旭、美团去了店铺事业群的先头技术人员。

---------结束---------

招聘信息

为商业群平台终端队伍招募FE、iOS、Android、Base北京、上海。 加入我们,你有机会服务美团的一些核心业务,参与多种渠道( H5、小程序、reactivenative、Hybrid )的业务开发,参与技术平台能力的沉淀,为多项业务同时提供领域能力的支持。 空间广阔,机会多,挑战难,是挑战自己,积累大企业平台化经验的绝好机会。 欢迎有兴趣的同学将简历发送给tech@meituan.com (邮件标题明确记载为“向店铺事业群平台终端招聘”)。

大家都在看

相关专题