QCon北京「鸿蒙专场」火热来袭!即刻报名,与创新同行~ 了解详情
写点什么

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

  • 2019-10-24
  • 本文字数:5548 字

    阅读完需:约 18 分钟

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.lambda0 这个在编译期间生成的静态私有方法。


至此,上面 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$1.class 反编译之后的代码如下:




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


注意:右图中 J8Sample.lambda0()方法在左图中没有显示出来,但是 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/-$J8SampleJ8Sample1 类型,只不过数字 1 变成了 Hash 值。



实现 Interface Ljava/lang/Runnable。Lcom/j8sample2/-$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 实现的方式,一探究竟。


作者介绍


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


本文转载自公众号美团技术团队(ID:meituantech)


原文链接


https://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651750839&idx=1&sn=6edeca6902f0d1c96826566f5eb7bc52&chksm=bd1258fa8a65d1ecafb812d2f2c874bcc93e1abd51072539e5b11ca81697478df602621af0d3&scene=27#wechat_redirect


2019-10-24 08:001798

评论

发布
暂无评论
发现更多内容

LigaAI X 猴子无限 | AIGC火了,人类又得到了什么?

LigaAI

分布式 大模型训练 研发协作平台 AIGC 大规模离散模型

超详细图文保姆级教程:App开发新手入门(一)

YonBuilder低代码开发平台

开发者 App 新手入门

明道云Sales Playbook开源版正式发布

明道云

WorkPlus即时通讯APP,提供智能化、多场景的IM系统解决方案

BeeWorks

高效、安全,华为云会议开启云上办公新天地

IT科技苏辞

云上办公兴起,华为云桌面Workspace更靠谱

IT科技苏辞

助力教育信创快速发展,统信软件与山东四所高校建立信创应用重点实验室

统信软件

软件 操作系统 教育 信创产业 教育新创

阿里云液冷技术荣获CDCC数据中心科技成果一等奖

云布道师

阿里云 基础设施建设

几类主流的虚拟化技术

穿过生命散发芬芳

虚拟化技术 11月月更

跬智信息 (Kyligence) 荣获信创“大比武”重要奖项,坚持做大做实国产软件

Kyligence

数据库 数据分析 云平台技术

普惠联接,让人类诗意地栖居在大地上

脑极体

第三章热备份路由选择协议(HSRP)

初学者

网络 11月月更

【Python 基础学习】-字符串

度假的小鱼

11月月更 Python字符串操作

Linux 文件与目录管理

芯动大师

创建资源文件 11月月更 Linux系统指令

手把手教你成为荣耀开发者:商户服务开通指南

荣耀开发者服务平台

android 开发者 手机 荣耀 honor

第五章TCP/IP 网络在我们身边

初学者

网络 11月月更

支持向量机-探索核函数的优势和缺陷

烧灯续昼2002

Python 机器学习 算法 sklearn 11月月更

Baklib知识库|为什么知识共享工具对减少内部知识缺口至关重要

Baklib

知识

案例 | 九科信息为某大型企业设计社保公积金自动缴存解决方案

九科Ninetech

一发一存一消费,跟着p8大佬深入学习Java中间件技术及其应用开发

钟奕礼

Java 程序员 java面试 java编程

统一移动办公门户,满足政企高效安全协作需求

BeeWorks

华为云会议AI智能降噪3.0,可抑制200种噪声!

科技之光

【Python 基础学习】-流程控制

度假的小鱼

11月月更 Python流程控制

【Python 基础学习】-数据类型

度假的小鱼

11月月更 Python数字类型

后台管理不可忽视,华为云会议最新支持管理员分权分域

秃头也爱科技

完全解析大数据的高可用集群部署

好程序员IT教育

大数据

HDC 2022 开发者主题演讲与技术分论坛干货分享(附课件)

HarmonyOS开发者

HarmonyOS

远程触发Jenkins的Pipeline任务

程序员欣宸

jenkins 11月月更 pipiline

第四章TCP/IP网络层设备路由器

初学者

网络 11月月更

高标准企业级安全性,华为云会议为线上沟通保驾护航

爱科技的水月

有了 Protocol buffer 还是用 JSON

HoneyMoose

Android兼容Java 8语法特性的原理分析_语言 & 开发_元合_InfoQ精选文章