重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
本篇文章为大家展示了怎么在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、filter、reduce等常见的高阶函数,数据源囊括了数组、集合、IO通道等,这些又为Java带来了流式编程或者说链式编程的风格,以上这些风格让Java变得越来越现代化和易用。
Android和Java关系
其实Java在Android的快速发展过程中扮演着非常重要的角色,无论是作为开发语言(Java)、开发Framework(Android-SDK引用了80%的JDK-API),还是开发工具(Eclipse or Android Studio)。这些都和Java有着千丝万缕的关系。不过可能是受到与Oracle的法律诉讼的影响,Google在Android上针对Java的升级一直都不是很积极:
Android 从1.0 一直升级到4.4,迭代了将近19个Android版本,才在4.4版本中支持了Java 7。
然后从Android 4.4版本开始算起,一直到Android N(7.0)共4个Android版本,才在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 Developer望眼欲穿。
接下来,本文将从技术原理层面,来分析一下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表达式原理
针对实例中的代码,我们来看下编译之后的字节码:
javac J8Sample.java -> J8Sample.class
javap -c -p J8Sample.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四大指令一起构成了虚拟机层面各种Java方法的分配调用指令集。区别在于:
后四种指令,在编译期间生成的class文件中,通过常量池(Constant Pool)的MethodRef常量已经固定了目标方法的符号信息(方法所属者及其类型,方法名字、参数顺序和类型、返回值)。虚拟机使用符号信息能直接解释出具体的方法,直接调用。
而invokedynamic指令在编译期间生成的class文件中,对应常量池(Constant Pool)的Invokedynamic_Info常量存储的符号信息中并没有方法所属者及其类型 ,替代的是BootstapMethod信息。在运行时, 通过引导方法BootstrapMethod机制动态确定方法的所属者和类型。这一特点也非常契合动态类型语言只有在运行期间才能确定类型的特征。
那么,invokedynamic如何通过引导方法找到所属者及其类型?我们依然结合前面的J8Sample实例:
javap -v J8Sample.class
结合J8Sample.class字节码,并对invokedynamic指令调用过程进行跟踪分析。总结如下:
依据上图invokedynamic调用步骤,我们一步一步做一个分析讲解。
步骤1 选取J8Sample.java源码中Lambda表达式1:Runnable runnable = () -> System.out.println("xixi"); // lambda表达式1
步骤2 通过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 标识的是class文件中的BootstrapMethod区域中引导方法的索引:
步骤5 引导方法中的java/lang/invoke/LambdaMetafactory.metafactory才是invokedynamic指令的关键:
该方法会在运行时,在内存中动态生成一个实现Lambda表达式对应函数式接口的实例类型,并在接口的实现方法中调用步骤2中新增的静态私有方法。
步骤6 使用java -Djdk.internal.lambda.dumpProxyClasses J8Sample.class运行一下,可以内存中动态生成的类型输出到本地:步骤7 通过javap -p -c J8Sample\$\$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编译器:
流程方面:都是按照如上图所示的官方流程进行Desugar的。
原理方面:却是参照Lambda在Java底层的实现,并将这些实现移至到RetroLambda插件或者Jack、D8编译器工具中。
下面我们逐个分析解读一下。
Android 间接支持之RetroLambda
如图所示,RetroLambda 的Desugar过程发生在javac将源码编译完成之后,dx工具进行dex编译之前。
RetroLambda Desugar
参照invokedynamic指令解读一节中的步骤5,根据java/lang/invoke/LambdaMetafactory.metafactory方法,直接将原本在运行时生成在内存中的J8Sample\$\$Lambda\$1.class,在javac编译结束之后,dx编译dex之前,直接生成到本地,并使用生成的J8Sample\$\$Lambda\$1类修改J8Sample.class字节码文件,将J8Sample.class中的invokedynamic指令替换成invokestatic指令。
将实例中的J8Sample.java放到一个配置了Retrolambda的Android工程中:
AndroidStudio -> Build -> make project 编译之后:
app:transformClassesWithRetrolambdaForDebug任务发生在app:compileDebugJavaWithJavac (javac)后,app:transformDexArchiveWithDexMergerForDebug (dx)之前,同时在build/intermediates/transforms/retrolambda下面生产如图所示的class文件。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编译器。并在Android Studio 3.1版本中默认使用D8作为dex的默认编译器。
D8 Desugar
如图所示,Desugar过程放在了D8的内部,由Android Studio这个IDE来实现这个转换,原理基本和RetroLambda是一样。
本质上也是参照java/lang/invoke/LambdaMetafactory.metafactory方法直接将原本在运行时生成在内存中的J8Sample\$\$Lambda\$1.class,在D8的编译dex期间,直接生成并写入到dex文件中。
同样,将实例中的J8Sample.java放到支持D8的Android工程中:
同样,AndroidStudio -> Build -> make project编译之后:
javac编译之后的J8Sample.class还是使用invokedynamic指令,即这一步并没有Desugar:
app:transformDexArchiveWithDexMergerForDebug(对应dx)任务之后,再对应build/intermediates/transforms/dexMerger目录找第0个classex.dex。
执行$ANDROID_HOME/build-tools/28.0.3/dexdump -d classes.dex >> dexInfo.txt拿到dex信息。
还是选取实例中Lambda表达式1 :Runnable runnable = () -> System.out.println("xixi");来进行分析。
这个dexIno.txt文件非常大,有1.4M,我们通过com.J8Smaple2.J8Sample找到我们J8Sample在dex中位置。新增方法:
J8Sample.main方法:
图中选中部分,对应就是Lambda表达式1 desugar之后的内容。
翻译成Java的话就变成了:new Lcom/j8sample2/-$$Lambda$J8Sample$jWmuYH0zEF070TKXrjBFgnnqOKc这个生成类的一个对象。类Lcom/j8sample2/-$$Lambda$J8Sample$jWmuYH0zEF070TKXrjBFgnnqOKc对应前面的生成的J8Sample$$Lambda$1类型,只不过数字1变成了Hash值。实现Interface Ljava/lang/Runnable。Lcom/j8sample2/-$$Lambda$J8Sample$jWmuYH0zEF070TKXrjBFgnnqOKc.run方法:
到这里,是不是和前面RetroLambda就一样了。
总结
至此,Lambda及其invokedynamic指令、RetroLambda插件、D8编译器各自的原理分析都已经结束了。
相比较Lambda在Java8自己内部的实现:即运行时,在内存中动态生成关联的函数式接口的实例类型,通过BSM-引导方法找到该内存类(字节码层面的反射)。
在Android上的其他三种Desugar方式,原理都是一样的,区别在于时机不同:
RetroLambda将函数式接口对应的实例类型的生产过程,放在javac编译之后,dx编译之前,并动态修改了表达式所属的字节码文件。
Jack&Jill是直接将接口对应的实例类型,直接jack过程中生成,并编译进了dex文件。
D8的过程是在dex编译过程中,直接在内存生成接口对应的实例类型,并将生成的类型直接写入生成的dex文件中。
上述内容就是怎么在Android中兼容Java 8语法,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注创新互联-成都网站建设公司行业资讯频道。