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 变得越来越现代化和易用。
其实 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 的。
想要更好的理解 Android 对 Java 8 的支持过程,Lambda 表达式这一代表性的“语法糖”是一个非常不错的切入点。所以,我们首先需要搞清楚 Lambda 表达式到底是什么?其底层的实现原理又是什么?
Lambda 表达式是 Java 支持函数式编程的基础,也可以称之为闭包。简单来说,就是在 Java 语法层面允许将函数当作方法的参数,函数可以当做对象。任一 Lambda 表达式都有且只有一个函数式接口与之对应,从这个角度来看,也可以说是该函数式接口的实例化。
通用格式:
简单范例:
说明:
针对实例中的代码,我们来看下编译之后的字节码:
javacJ8Sample.java ->J8Sample.class javap -c -pJ8Sample.class
从字节码中我们可以看到:
可见,Lambda 表达式在虚拟机层面上,是通过一种名为 invokedynamic 字节码指令来实现的。那么 invokedynamic 又是何方神圣呢?
invokedynamic 指令是 Java 7 中新增的字节码调用指令,作为 Java 支持动态类型语言的改进之一,跟 invokevirtual、invokestatic、invokeinterface、invokespecial 四大指令一起构成了虚拟机层面各种 Java 方法的分配调用指令集。区别在于:
那么,invokedynamic 如何通过引导方法找到所属者及其类型?我们依然结合前面的 J8Sample 实例:
javap -vJ8Sample.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 m a i n
0 这个在编译期间生成的静态私有方法。至此,上面 7 个步骤就是 Lambda 表达式在 Java 的底层的实现原理。Android 针对这些实现会怎么处理呢?
回到 Android 系统上,Java-Bytecode(JVM 字节码)是不能直接运行在 Android 系统上的,需要转换成 Android-Bytecode(Dalvik/ART 字节码)。
如图:
通过 Lambda 这节,我们知道 Java 底层是通过 invokedynamic 指令来实现,由于 Dalvik/ART 并没有支持 invokedynamic 指令或者对应的替代功能。简单的来说,就是 Android 的 dex 编译器不支持 invokedynamic 指令,导致 Android 不能直接支持 Java 8。
既然不能直接支持,那就只能在 Java-Bytecode 转换到 Android-Bytecode 这一过程中想办法,间接支持。这个间接支持的过程我们统称为 Desugar(脱糖)过程。
官方流程图:
当前,无论是 RetroLambda,还是 Google 的 Jack & Jill 工具,还是最新的 D8 dex 编译器:
下面我们逐个分析解读一下。
如图所示,RetroLambda 的 Desugar 过程发生在 javac 将源码编译完成之后,dx 工具进行 dex 编译之前。
参照 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 m a i n
0() 方法在左图中没有显示出来,但是 J8Sample.class 字节码确实是存在的。
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 的默认编译器。
如图所示,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/- L a m b d a $ J 8 S a m p l e $ j W m u Y H 0 z E F 070 T K X r j B F g n n q O K c 这 个 生 成 类 的 一 个 对 象 。 类 L c o m / j 8 s a m p l e 2 / −
Lambda J 8 S a m p l e 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,还是 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