怎么在Android中兼容Java 8语法

发布时间:2021-05-26 11:51:14 作者:Leah
来源:亿速云 阅读:416

本篇文章为大家展示了怎么在Android中兼容Java 8语法,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。

Java 8概述

Java 8是Java开发语言非常重要的一个版本。Oracle从2014年3月18日发布Java 8,从该版本起,Java开始支持函数式编程。特别是吸收了运行在JVM上的Scala、Groovy等动态脚本语言的特性之后,Java 8在语言的表达力、简洁性两个方面有了很大的提高。

Java 8的主要语言特性改进概括起来包括以下几点:

其中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的升级一直都不是很积极:

可谓“历经坎坷”。特别是Rx大行其道的今天,Rx配合Java 8特性Lambda带来简洁、高效的开发体验,更是让Android Developer望眼欲穿。

接下来,本文将从技术原理层面,来分析一下Android是如何支持Java 8的。

Lambda 表达式

想要更好的理解Android对Java 8的支持过程,Lambda表达式这一代表性的“语法糖”是一个非常不错的切入点。所以,我们首先需要搞清楚Lambda表达式到底是什么?其底层的实现原理又是什么?

Lambda表达式是Java支持函数式编程的基础,也可以称之为闭包。简单来说,就是在Java语法层面允许将函数当作方法的参数,函数可以当做对象。任一Lambda表达式都有且只有一个函数式接口与之对应,从这个角度来看,也可以说是该函数式接口的实例化。

Lambda表达式

通用格式:

怎么在Android中兼容Java 8语法

简单范例:

怎么在Android中兼容Java 8语法

怎么在Android中兼容Java 8语法

说明:

Lambda表达式原理

针对实例中的代码,我们来看下编译之后的字节码:

javac J8Sample.java  ->  J8Sample.class

javap -c -p J8Sample.class 
怎么在Android中兼容Java 8语法

从字节码中我们可以看到:

可见,Lambda表达式在虚拟机层面上,是通过一种名为invokedynamic字节码指令来实现的。那么invokedynamic又是何方神圣呢?

invokedynamic 指令解读

invokedynamic指令是Java 7中新增的字节码调用指令,作为Java支持动态类型语言的改进之一,跟invokevirtual、invokestatic、invokeinterface、invokespecial四大指令一起构成了虚拟机层面各种Java方法的分配调用指令集。区别在于:

那么,invokedynamic如何通过引导方法找到所属者及其类型?我们依然结合前面的J8Sample实例:

javap -v J8Sample.class
怎么在Android中兼容Java 8语法

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

怎么在Android中兼容Java 8语法

依据上图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中发现了新增的私有静态方法:
怎么在Android中兼容Java 8语法

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

#2 = InvokeDynamic      #0:#35         // #0:run:()Ljava/lang/Runnable;   

注意,这里InvokeDynamic不是指令,代表的是Constant_InvokeDynamic_Info结构。

步骤4 结构后面紧跟的 #0 标识的是class文件中的BootstrapMethod区域中引导方法的索引:

怎么在Android中兼容Java 8语法

步骤5 引导方法中的java/lang/invoke/LambdaMetafactory.metafactory才是invokedynamic指令的关键:

怎么在Android中兼容Java 8语法

怎么在Android中兼容Java 8语法

该方法会在运行时,在内存中动态生成一个实现Lambda表达式对应函数式接口的实例类型,并在接口的实现方法中调用步骤2中新增的静态私有方法。

步骤6 使用java -Djdk.internal.lambda.dumpProxyClasses J8Sample.class运行一下,可以内存中动态生成的类型输出到本地:

怎么在Android中兼容Java 8语法

步骤7 通过javap -p -c J8Sample\$\$Lambda\$1.class反编译一下,可以看到生成类的实现:

怎么在Android中兼容Java 8语法
在run方法中使用了invokestatic指令,直接调用了J8Sample.lambda$main$0这个在编译期间生成的静态私有方法。

至此,上面7个步骤就是Lambda表达式在Java的底层的实现原理。Android 针对这些实现会怎么处理呢?

Android不能直接支持

回到Android系统上,Java-Bytecode(JVM字节码)是不能直接运行在Android系统上的,需要转换成Android-Bytecode(Dalvik/ART 字节码)。

如图:

怎么在Android中兼容Java 8语法

通过Lambda这节,我们知道Java底层是通过invokedynamic指令来实现,由于Dalvik/ART并没有支持invokedynamic指令或者对应的替代功能。简单的来说,就是Android的dex编译器不支持invokedynamic指令,导致Android不能直接支持Java 8。

Android间接支持

既然不能直接支持,那就只能在Java-Bytecode转换到Android-Bytecode这一过程中想办法,间接支持。这个间接支持的过程我们统称为Desugar(脱糖)过程。

官方流程图:

怎么在Android中兼容Java 8语法

当前,无论是RetroLambda,还是Google的Jack & Jill 工具,还是最新的D8 dex编译器:

下面我们逐个分析解读一下。

Android 间接支持之RetroLambda

怎么在Android中兼容Java 8语法

如图所示,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工程中:

怎么在Android中兼容Java 8语法

AndroidStudio -> Build -> make project 编译之后:

怎么在Android中兼容Java 8语法

app:transformClassesWithRetrolambdaForDebug任务发生在app:compileDebugJavaWithJavac (javac)后,app:transformDexArchiveWithDexMergerForDebug (dx)之前,同时在build/intermediates/transforms/retrolambda下面生产如图所示的class文件。

J8Sample.class和J8Sample$$Lambda$1.class反编译之后的代码如下:

怎么在Android中兼容Java 8语法

怎么在Android中兼容Java 8语法

通过反编译代码,可以看出J8Sample.class中Lambda表达式已经被我们熟悉的1.7or1.6的语句所替代。

注意:右图中J8Sample.lambda$main$0()方法在左图中没有显示出来,但是J8Sample.class字节码确实是存在的。
Android间接支持之Jack&Jill工具
怎么在Android中兼容Java 8语法

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

怎么在Android中兼容Java 8语法

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工程中:

怎么在Android中兼容Java 8语法

同样,AndroidStudio -> Build -> make project编译之后:

怎么在Android中兼容Java 8语法

javac编译之后的J8Sample.class还是使用invokedynamic指令,即这一步并没有Desugar:

怎么在Android中兼容Java 8语法

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中位置。
怎么在Android中兼容Java 8语法

新增方法:

怎么在Android中兼容Java 8语法

J8Sample.main方法:

怎么在Android中兼容Java 8语法

图中选中部分,对应就是Lambda表达式1 desugar之后的内容。

翻译成Java的话就变成了:new Lcom/j8sample2/-$$Lambda$J8Sample$jWmuYH0zEF070TKXrjBFgnnqOKc这个生成类的一个对象。类Lcom/j8sample2/-$$Lambda$J8Sample$jWmuYH0zEF070TKXrjBFgnnqOKc对应前面的生成的J8Sample$$Lambda$1类型,只不过数字1变成了Hash值。
怎么在Android中兼容Java 8语法

实现Interface Ljava/lang/Runnable。Lcom/j8sample2/-$$Lambda$J8Sample$jWmuYH0zEF070TKXrjBFgnnqOKc.run方法:

怎么在Android中兼容Java 8语法

到这里,是不是和前面RetroLambda就一样了。

总结

至此,Lambda及其invokedynamic指令、RetroLambda插件、D8编译器各自的原理分析都已经结束了。

相比较Lambda在Java8自己内部的实现:即运行时,在内存中动态生成关联的函数式接口的实例类型,通过BSM-引导方法找到该内存类(字节码层面的反射)。

在Android上的其他三种Desugar方式,原理都是一样的,区别在于时机不同:

  1. RetroLambda将函数式接口对应的实例类型的生产过程,放在javac编译之后,dx编译之前,并动态修改了表达式所属的字节码文件。

  2. Jack&Jill是直接将接口对应的实例类型,直接jack过程中生成,并编译进了dex文件。

  3. D8的过程是在dex编译过程中,直接在内存生成接口对应的实例类型,并将生成的类型直接写入生成的dex文件中。


上述内容就是怎么在Android中兼容Java 8语法,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注亿速云行业资讯频道。

推荐阅读:
  1. vue兼容IE报错怎么办
  2. 怎么在Java 8中创建Stream流

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

android java

上一篇:如何在java中使用BeanUtils.copyProperties

下一篇:使用css怎么制作一个3D照片墙效果

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》