Android 兼容 Java 8 语法特性的原理分析

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

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.classjavap -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具体分析了。

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方式,原理都是一样的,区别在于时机不同:

  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实现的方式,一探究竟。

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

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

相关文章

由Actvity启动模式之SingleInstance引起的坑的反思

以前对于Activity的启动模式只限于文字说明理解的层面,但最近的一个问题让我对启动模式有了更深的了解。 Activity有四大启动模式:standard,singleTop,singleTask,singleInstance,这些启动模式的特点就不在这里细说,主要说一下SingleInstanc…

“李宏毅老师对不起,我要去追这门美女老师的课了” ,台大陈蕴侬深度学习课程最新资料下载...

估计很多同学看到台大首先想到是李宏毅老师的“深度学习人类语言处理”课程,不过这次我们说的是台湾大学陈蕴侬老师的“应用深度学习”课程,这门课程我们之前推荐过,不过主要给大家推荐的是课程视频和课件资源。前段时间,我把这门…

超级好用的使用python批量更新MYsql,速度从一万条需要一天变道一万条需要10分钟左右

推荐网址:https://www.jb51.net/article/100747.html MySQL批量插入的语法为: UPDATE nine_tupleSET SENTI_VALUE CASE N_T_ID WHEN 1 THEN ‘1.57’ WHEN 2 THEN ‘1.3’ WHEN 3 THEN ‘1.2’ END WHERE N_T_ID IN (1,2,3)主要思想:将42W条…

论文浅尝 | 使用位置敏感的序列标注联合抽取实体和重叠关系

论文笔记整理:余海阳,浙江大学硕士,研究方向为知识图谱、自然语言处理。链接:https://wvvw.aaai.org/ojs/index.php/AAAI/article/view/4591动机之前的联合抽取实体以及实体间的关系的方法,主要是采用管道模型&#xf…

LeetCode 938. 二叉搜索树的范围和(二叉树遍历+搜索剪枝)

文章目录1. 题目2. 解题递归剪枝中序遍历循环剪枝1. 题目 给定二叉搜索树的根结点 root,返回 L 和 R(含)之间的所有结点的值的和。 题目的意思,节点的值在[L, R]这个区间内,就加到结果里,求所有符合条件的…

OneData建设探索之路:SaaS收银运营数仓建设

背景 随着业务的发展,频繁迭代和跨部门的垂直业务单元变得越来越多。但由于缺乏前期规划,导致后期数仓出现了严重的数据质量问题,这给数据治理工作带来了很大的挑战。在数据仓库建设过程中,我们总结的问题包括如下几点&#xff1a…

Android远程服务与本地服务的特点以及功能差异

最近又重新看了点IPC的相关知识,以前看过不少Binder的知识,看到c层思路就很模糊了,回头再看也算是一种学习。 IPC全称为:interprocess communication内部进程间通信,官方地址:http://developer.android.com/intl/zh-c…

svn中文语言包安装(最详细步骤)+Language Pack+TortoiseSVN 安装

原文链接:https://blog.csdn.net/massillon/article/details/102780782 svn中文语言包安装(最详细步骤) 一. 查看自己的SVN 版本(这里省略也可以,同事1.9版本的直接在官网下载的语言包也能用,区别对待把,可以先直接在官网下载,不行再去找对应版本语言包) 1.打开SVN找…

我拿模型当朋友,模型却想泄漏我的隐私?

文 | 阿毅编 | 小轶相信大家对Facebook–Cambridge Analytica隐私泄露事件都还有印象。这事儿在当时可谓爆炸性新闻,激起了公众对数据隐私的强烈关注,也间接影响了美国总统选举结果(这不是重点)。不过从事后诸葛亮来看&#xff0c…

论文浅尝 | SPARQL 语言的 ASK 查询表达性研究进展

论文作者之一:杨炫兴,天津大学博士生。链接:http://cic.tju.edu.cn/faculty/zhangxiaowang/publication/ASK.pdf动机SPARQL是万维网联盟(World Wide Web Consortium,简记W3C)推荐的知识图谱标准查询语言&am…

LeetCode 1021. 删除最外层的括号(栈)

文章目录1. 题目2. 解题1. 题目 题目链接 示例 1:输入:"(()())(())" 输出:"()()()" 解释: 输入字符串为 "(()())(())",原语化分解得到 "(()())" "(())"&#xf…

JVM CPU Profiler技术原理及源码深度解析

研发人员在遇到线上报警或需要优化系统性能时,常常需要分析程序运行行为和性能瓶颈。Profiling技术是一种在应用运行时收集程序相关信息的动态分析手段,常用的JVM Profiler可以从多个方面对程序进行动态分析,如CPU、Memory、Thread、Classes、…

Facebook提出生成式实体链接、文档检索,大幅刷新SOTA!

文 | 花小花Posy导言最近ICLR的rebutal 前后分数对比出来了,很多评委都改了分数,有改多的,也有改少的。今天给大家介绍的这篇高分论文竟然在rebuttal前后都保持高分,证明评委们对它的认可程度是很高的。实体检索任务的定义是&…

论文浅尝 | 知识库问答中关系检测的学习表示映射

论文笔记整理:吴涵,天津大学硕士,研究方向:自然语言处理。链接:Paper: https://arxiv.org/pdf/1907.07328v1.pdfCode: https://github.com/wudapeng268/KBQA-Adapter引入在关系检测任务中,对于训练数据中已…

LeetCode 1175. 质数排列

文章目录1. 题目2. 解题1. 题目 请你帮忙给从 1 到 n 的数设计排列方案,使得所有的「质数」都应该被放在「质数索引」(索引从 1 开始)上;你需要返回可能的方案总数。 让我们一起来回顾一下「质数」:质数一定是大于 1…

ICLR最高分论文揭秘模型泛化,GNN是潜力股

文 | Jerry Qiu编 | 小轶我们都知道,人类在很多任务上都可以很好地完成“外推”,例如:啊不——我是说——例如,我们学会两位数的加减乘除后,就可以轻松将其推广至任意大整数的四则运算:从数学的角度来讲&am…

Android系统原生应用解析之桌面闹钟及相关原理应用之时钟任务的应用(一)

前段时间我一个朋友在面试回来问我:那个公司要5天之内完成一个项目,功能包括每天早上6点开始执行定时任务,大批量图片上传,大批量数据库同步。我心想,后两个功能还好说,可就是每天早上6点开始执行的这种定时…

配送交付时间轻量级预估实践

1. 背景 可能很多同学都不知道,从打开美团App点一份外卖开始,然后在半小时内就可以从骑手小哥手中拿到温热的饭菜,这中间涉及的环节有多么复杂。而美团配送技术团队的核心任务,就是将每天来自祖国各地的数千万份订单,迅…

论文浅尝 | 面向 cQA 的跨语言问题检索方法

论文笔记整理:谭亦鸣,东南大学博士生,研究方向为知识库问答。来源:WWW2019链接:https://dl.acm.org/citation.cfm?doid3308558.3313502本文提出了一种面向cQA的跨语言问题检索方法,旨在对于给定的问题检索…

揭秘Python并发编程——协程

原文链接:https://baijiahao.baidu.com/s?id1649450510185145678&wfrspider&forpc Python并发编程一直是进阶当中不可跨越的一道坎,其中包括进程、线程、协程,今天我们就来聊一聊协程。协程的定义很简单,从头到尾只有一条…