java8 lambda map排序_Android兼容Java 8语法特性的原理分析

本文主要阐述了Lambda表达式及其底层实现(invokedynamic指令)的原理、Android第三方插件RetroLambda对其的支持过程、Android官方最新的dex编译器D8对其的编译支持。通过对这三个方面的跟踪分析,以Java 8的代表性特性——Lambda表达式为着眼点,将Android如何兼容Java8的过程分享给大家。

d7125b76a47549ddc4e41508b500b09c.png

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表达式

通用格式:

169d7b1ec766ca5f04f55e157b18ce50.png

简单范例:

8f2e47b139e1d946fdcb305bd4bdaab7.png
8c10822c752e2978e98166fcc56bdbc8.png

说明:

  • Lambda表达式中 () 对应的是函数式接口-run方法的参数列表。
  • Lambda表达式中 System.out.println("xixi") / System.out.println("haha"),在运行时会是具体的run方法的实现。

Lambda表达式原理

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

javac J8Sample.java -> J8Sample.classjavap -c -p J8Sample.class 
640253edacc73e64b391178570adbb1e.png

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

  • 实例中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 
12c235df9daebe83795c088d13909aa2.png

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

9d0357bf3c5ba2c7b4ce15848266381c.png

依据上图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中发现了新增的私有静态方法:

aa4c933acc1333f20b7dd7a9657f9a85.png

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

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

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

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

4f472bfc589159a9dbc7aaf477288035.png

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

65c17a7b20acc323a47b5fa0070bcd1e.png
8ba0a47d91b833d50230f87fdd24c5cf.png

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

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

2a2c73cf768fc8fa7c2e9dd760bc7b34.png

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

18cfd97d0f0faef0ed5ddd0e783dcdd3.png

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

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

Android不能直接支持

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

如图:

775ad30d6ea5c678fbe122ff18b40a28.png

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

Android间接支持

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

官方流程图:

b6e0b9951dcb870fbd03ff6204560462.png

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

  • 流程方面:都是按照如上图所示的官方流程进行Desugar的。
  • 原理方面:却是参照Lambda在Java底层的实现,并将这些实现移至到RetroLambda插件或者Jack、D8编译器工具中。

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

Android 间接支持之RetroLambda

d36f8afc9bd23858d12ff597019e3022.png

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

fc27917a178cfe98cf4afdef0b5f2d47.png

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

7ba022cdcf916e66374b5b79e17df1f2.png

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

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

1b5f3c51e943e130abf6761193c0d778.png
bb885c1126a49a6de74e328e24686653.png

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

注意:右图中J8Sample.lambda$main$0()方法在左图中没有显示出来,但是J8Sample.class字节码确实是存在的。

Android间接支持之Jack&Jill工具

7a7833ee2c191af66d0afd601f53b100.png

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

01552c3a59aae1507cd08af3b372ec30.png

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

914f8f9a289508d509bec2657d2acd5c.png

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

6de01621f91a6d48910296f2b077e2f4.png

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

136afe948eb3f8da5955ac76e3f8fb41.png

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中位置。

1337ab5b854d538d7bf922adb28f870e.png

新增方法:

21c2fea3b926c26a39ff2076a8f7582d.png

J8Sample.main方法:

78906bdd4e5e547493366fc7a23eceb3.png

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

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

73406c9c337235b448661b09fcad2a4f.png

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

56c0b463b21166a28905af79c27ee3bf.png

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

总结

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

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

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

  1. RetroLambda将函数式接口对应的实例类型的生产过程,放在javac编译之后,dx编译之前,并动态修改了表达式所属的字节码文件。
  2. Jack&Jill是直接将接口对应的实例类型,直接jack过程中生成,并编译进了dex文件。
  3. D8的过程是在dex编译过程中,直接在内存生成接口对应的实例类型,并将生成的类型直接写入生成的dex文件中。

探讨

无论是RetroLambda,还是D8,对Java8的特性也不是全都支持。

Java8新增的许多API(例如:新的DataAPI),就D8编译器而言,只有在Android P(9.0)版本中能直接运行。低于9.0就不行了。如何能够全版本支持Java 8。D8还有很长的一段路要走。

如果我们在低版本需要使用新的API,目前可以采取将这些API打包进去的临时办法。

写到这里,肯定有人要提出,为什么不直接使用Kotlin呢?确实Kotlin对Lambda表达式、函数引用等特性都做了很好的支持,但是现实的情况中,Kotlin很难取代Android中的Java。新业务、新工程还相对容易,对老业务来说,尤其是经过多年沉淀,工程结构复杂,迁移改造带来的收益,往往远远小于迁移改造带来的成本和不可控之风险。Kotlin和Java同时存在的情况,长期来看是一个必然的结果。

至于Java 8的其他特性呢,D8是如何实现的,也可以按照上面类似的方式去分析,甚至可以结合Kotlin实现的方式,一探究竟。

作者简介

元合、朝旭,美团到店事业群前端工程师。

---------- END ----------

招聘信息

到店事业群平台终端团队诚招FE、iOS、Android,Base北京、上海。加入我们,你有机会服务美团的多个核心业务,涉及多端多渠道(H5、小程序、React Native、Hybrid)的业务开发,参与技术平台能力沉淀,为多个业务同时提供领域能力支撑。空间大、机会多、挑战难,这是挑战自己并积累大企业平台化经验的绝佳机会。欢迎有兴趣的同学投送简历到 tech@meituan.com(邮件标题注明:到店事业群平台终端招聘)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/506251.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

lrange是取出所有值并移除么_部落冲突:兵营容量提升,移除超级部队、英雄防御状态冷却时间...

爱生活,爱游戏,大家好,我是阿呆!部落冲突已经好长时间没更新了,这次秋天不再等待,部落冲突第二弹更新来袭,被称之为今年最赞的一次更新,感觉和我一起来看看吧!防御建筑升…

python爬取百度迁徙数据_python爬虫-动态爬取百度迁徙

#1.模拟浏览器发送请求 importrequestsimportjsonimportpandas as pd city_name[] province_name[] value[] url https://huiyan.baidu.com/migration/cityrank.jsonp?dtcountry&id0&typemove_in&date20200315&callbackjsonp_1590404076900_7186798rqrequests…

计算机系统硬盘内存主频,内存时序和频率_内存硬盘-中关村在线

内存时序和频率内存时序是描述内存条性能的一种参数,一般存储在内存条的SPD中。一般数字“A-B-C-D”分别对应的参数是“CL-tRCD-tRP-tRAS”,它们的含义依次为:CAS Latency(简称CL值)内存CAS延迟时间,它是内存的重要参数之一&#…

ubuntun系统mysql数据库同步_Canal 实现 Mysql数据库实时数据同步

简介1.1 canal介绍​ Canal是一个基于MySQL二进制日志的高性能数据同步系统。Canal广泛用于阿里巴巴集团(包括https://www.taobao.com),以提供可靠的低延迟增量数据管道,github地址:https://github.com/alibaba/canalC…

int数组初始化_Java数组

###理解数组Java的数组要求所有的元素为相同数据类型。既可以存储基本类型的数据,也可以存储引用类型的数据,只要所有元素的类型相同就可以。一旦数组的初始化完成,数组在内存中占用的空间就被固定下来,因此数组长度不可变&#x…

各自然带代表植被_植被垂直带谱?水与热之间的较量。

随着海拔高度的上升,从山麓到山顶年平均气温逐渐降低,生长季节逐渐缩短,同时在一定海拔范围内随着降水量的增加,风速加大,辐射增强,土壤条件也发生相应的变化。在以上因素的综合作用下,植被表现…

ora00936缺失表达式怎么解决_初学者学习AE经常会遇到问题及解决方法,快进来看看吧!~~...

关于软件常见问题请一定要收藏对初学者的你绝对有用首先解决的第一个问题是:软件安装失败提示及解决方案说起原因安装失败是由AE的某些文件没有删除干净(绿化版本极易造成,不建议安装)解决方案(所有Adobe软件通用)如果安装过Adobe公司的软件需要在以下路…

matlab画出周期为2的方波图形 傅立叶级数_高等数学系列R之四:傅立叶级数及变换...

2020-03-23傅立叶级数是将周期函数表示成由多个 (或无穷多个) 不同频率的正弦函数和余弦函数的线性组合,这些不同的频率是不连续的,例如傅立叶级数:,其 sin 内的 x, 3x, 5x 是不连续的。而傅立叶积分是将傅立叶级数延伸到非周期函…

两线怎么接三线插座图_什么是RTD热电阻?两线制和三线制RTD有什么不同?

RTD的英文全称为“Resistance Temperature Detector”,因此准确来说,它应该翻译为“电阻温度检测器”。RTD是一种特殊的电阻,其阻值会随着温度的升高而变大,随着温度的降低而减小。工业上利用它的这一特性进行温度测量&#xff0c…

计算机内存条价格,最新内存条天梯图2020 内存条全面选购指南

*文章末尾有内存条推荐。电脑内存(RAM)是程序运行的地方,当你打开一款软件、游戏时,系统会将硬盘中的必要数据复制到内存中,CPU 再从内存中获取数据,因为内存的速度比硬盘要快的多多多。唯一的遗憾是每次内存断电,其中…

eclipse html插件_欲善事先利器——IDEA 插件篇

工欲善其事,必先利其器,好鞋踢好球是非常合乎逻辑的事情。——《长江七号》同样的开场白,不一样的酒,不一样的故事。上篇《欲善事先利器——系统篇》已经推荐了一些个人常用的效率系统软件。觉得有帮助的,有共鸣的 Roc…

怎么挪动_2020蚕茧多少钱一斤,蚕茧怎么做成蚕丝被

href"http://www.88360.com/product/bplist-1904/splist-1904-2063/">蚕茧 又被称作蚕衣,指包囊桑蚕的茧,为蚕蛹阶段的防护层,在其中包含茧衣、茧层及其蛹衬等一部分,茧层能够缫丝,茧衣及缫制后的废丝能用…

flash软件视频不能测试,360安全卫士解决视频播放错误怎么办?无法调出flash解决办法...

使用电脑继续观看视频的时候,小伙伴们通常都会遇到一件事,那就是视频突然的播放错误,这个时候怎么使用360安全卫士解决,因此就让小编给大家详细的讲讲解决办法吧视频播放错误解决办法介绍【简易步骤】:【360安全卫士】…

autohotkey实例+懒人包_懒人福音——GitHub 热点速览 Vol.42

作者:HelloGitHub-小鱼干懒人福音是什么?就是省时省事,正如 Waypoint 一样,你不需要在多个平台构建代码即可部署发布应用,它允许你将应用程序构建、部署和发布生命周期定义为代码。Bit 则让你不需要记 git 操作&#x…

蓝牙连接不上车要hfp_鹅厂又要霸屏,连接四部剧将袭,冲着主创颜值不追不行啦...

对于鹅厂的电视剧,相信观众们都有看过不少吧!其中近期热播的《小风暴之时间的玫瑰》这部高甜的影视剧就备受观众们喜爱了。毕竟《小风暴》不管是剧情还是剧中的人物选角都让我们非常满意,剧中有胡一天和乔欣这两位高颜值主演疯狂“撒狗粮”的…

ios服务器需要开启ipv6的支持,针对iOS审核要求为应用兼容IPv6

开发中使用IPv6的好处1、IPv6:避免了网络地址转换(NAT)2、通过网络通过使用简化的头提供了更快的路由3、防止网络分段4、避免广播邻居地址解析不建议使用底层的网络API下图展示的蓝色部分的这些API都是不存在兼容性问题的,而我们平时自己用的包括那些第三方的网络库…

verilog换行太长代码_Verilog 之 File I/O task and function

不点蓝字,何来故事?许久之前关于Verilog I/O操作的一篇笔记梳理,再此与诸君共分享。0I/O操作类型verilog中关于文件操作的任务和函数主要分为四类:(1)打开和关闭文件的任务和函数(2) 向文件中输入信息的任务(3) 向变量中输入信息的…

MFC读取文件数据,添加信息到列表并保存到文件

打开并读取文件信息 添加: BOOL infoDlg::OnInitDialog() {CDialogEx::OnInitDialog();// TODO: 在此添加额外的初始化AfxMessageBox("欢迎查看学生信息");SetList();return TRUE; // return TRUE unless you set the focus to a control// 异常: OCX 属…

nginx 上传 文件超时设置_Ingressnginx自定义配置文件

点击上方蓝色字体,关注我们读完需 8 分钟速读需 4 分钟k8s集群默认安装的ingress-nginx直接投入测试或生产使用,其不合适的配置参数可能会导致一些访问报错。例如:“413 Request Entity Too Large”“503 Service Unavailable”此时我们就需要…

fiddler安装_Fiddler的安装和APP抓包

点击上方“蓝字”关注我们吧!前言1.Fiddler安装包2.安卓手机3.iOS手机1.下载fiddler软件:可以去官网下载https://www.telerik.com/fiddler,可以下载最新版2.百度云盘(非最新版):链接:https://pan.baidu.com/s/10XVzoxw…