自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

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

企業(yè)動態(tài)
本文主要闡述了Lambda表達(dá)式及其底層實(shí)現(xiàn)(invokedynamic指令)的原理、Android第三方插件RetroLambda對其的支持過程、Android官方最新的dex編譯器D8對其的編譯支持。

 本文主要闡述了Lambda表達(dá)式及其底層實(shí)現(xiàn)(invokedynamic指令)的原理、Android第三方插件RetroLambda對其的支持過程、Android官方最新的dex編譯器D8對其的編譯支持。通過對這三個(gè)方面的跟蹤分析,以Java 8的代表性特性——Lambda表達(dá)式為著眼點(diǎn),將Android如何兼容Java8的過程分享給大家。

[[326076]]

Java 8概述

Java 8是Java開發(fā)語言非常重要的一個(gè)版本。Oracle從2014年3月18日發(fā)布Java 8,從該版本起,Java開始支持函數(shù)式編程。特別是吸收了運(yùn)行在JVM上的Scala、Groovy等動態(tài)腳本語言的特性之后,Java 8在語言的表達(dá)力、簡潔性兩個(gè)方面有了很大的提高。

Java 8的主要語言特性改進(jìn)概括起來包括以下幾點(diǎn):

  • Lambda表達(dá) (函數(shù)閉包)
  • 函數(shù)式接口 (@FunctionalInterface)
  • Stream API (通過流式調(diào)用支持map、filter等高階函數(shù))
  • 方法引用(使用::關(guān)鍵字將函數(shù)轉(zhuǎn)化為對象)
  • 默認(rèn)方法(抽象接口中允許存在default修飾的非抽象方法)
  • 類型注解和重復(fù)注解

其中Lambda表達(dá)、函數(shù)式接口、方法引用三個(gè)特性為Java帶來了函數(shù)式編程的風(fēng)格;而Stream實(shí)現(xiàn)了map、filter、reduce等常見的高階函數(shù),數(shù)據(jù)源囊括了數(shù)組、集合、IO通道等,這些又為Java帶來了流式編程或者說鏈?zhǔn)骄幊痰娘L(fēng)格,以上這些風(fēng)格讓Java變得越來越現(xiàn)代化和易用。

Android和Java關(guān)系

其實(shí)Java在Android的快速發(fā)展過程中扮演著非常重要的角色,無論是作為開發(fā)語言(Java)、開發(fā)Framework(Android-SDK引用了80%的JDK-API),還是開發(fā)工具(Eclipse or Android Studio)。這些都和Java有著千絲萬縷的關(guān)系。不過可能是受到與Oracle的法律訴訟的影響,Google在Android上針對Java的升級一直都不是很積極:

  • Android 從1.0 一直升級到4.4,迭代了將近19個(gè)Android版本,才在4.4版本中支持了Java 7。
  • 然后從Android 4.4版本開始算起,一直到Android N(7.0)共4個(gè)Android版本,才在Jack/Jill工具鏈勉強(qiáng)支持了Java 8。但由于Jack/Jill工具鏈在構(gòu)建流程中舍棄了原有Java字節(jié)碼的體系,導(dǎo)致大量既有的技術(shù)沉淀無法應(yīng)用,致使許多App工程放棄了接入。
  • 最后直到Android P(9.0)版本, Google 才在Android Studio 3.x中通過新增的D8 dex編譯器正式支持了Java 8,但部分API并不能全版本支持。

可謂”歷經(jīng)坎坷“。特別是Rx大行其道的今天,Rx配合Java 8特性Lambda帶來簡潔、高效的開發(fā)體驗(yàn),更是讓Android Developer望眼欲穿。

接下來,本文將從技術(shù)原理層面,來分析一下Android是如何支持Java 8的。

Lambda 表達(dá)式

想要更好的理解Android對Java 8的支持過程,Lambda表達(dá)式這一代表性的”語法糖“是一個(gè)非常不錯(cuò)的切入點(diǎn)。所以,我們首先需要搞清楚Lambda表達(dá)式到底是什么?其底層的實(shí)現(xiàn)原理又是什么?

Lambda表達(dá)式是Java支持函數(shù)式編程的基礎(chǔ),也可以稱之為閉包。簡單來說,就是在Java語法層面允許將函數(shù)當(dāng)作方法的參數(shù),函數(shù)可以當(dāng)做對象。任一Lambda表達(dá)式都有且只有一個(gè)函數(shù)式接口與之對應(yīng),從這個(gè)角度來看,也可以說是該函數(shù)式接口的實(shí)例化。

Lambda表達(dá)式

通用格式:

 

簡單范例:

 

說明:

  • Lambda表達(dá)式中 () 對應(yīng)的是函數(shù)式接口-run方法的參數(shù)列表。
  • Lambda表達(dá)式中 System.out.println(“xixi”) / System.out.println(“haha”),在運(yùn)行時(shí)會是具體的run方法實(shí)現(xiàn)。

Lambda表達(dá)式原理

針對實(shí)例中的代碼,我們來看下編譯之后的字節(jié)碼:

  1. javac J8Sample.java  ->  J8Sample.class 
  2. javap -c -p J8Sample.class  

從字節(jié)碼中我們可以看到:

  • 實(shí)例中 Lambda表達(dá)式1變成了字節(jié)碼代碼塊中 Line 11的 0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable。
  • 實(shí)例中 Lambda表達(dá)式2變成了字節(jié)碼代碼塊中 Line 20的 21: invokedynamic #6, 0 // InvokeDynamic #1:run:()Ljava/lang/Runnable。

可見,Lambda表達(dá)式在虛擬機(jī)層面上,是通過一種名為invokedynamic字節(jié)碼指令來實(shí)現(xiàn)的。那么invokedynamic又是何方神圣呢?

invokedynamic 指令解讀

invokedynamic指令是Java 7中新增的字節(jié)碼調(diào)用指令,作為Java支持動態(tài)類型語言的改進(jìn)之一,跟invokevirtual、invokestatic、invokeinterface、invokespecial四大指令一起構(gòu)成了虛擬機(jī)層面各種Java方法的分配調(diào)用指令集。區(qū)別在于:

  • 后四種指令,在編譯期間生成的class文件中,通過常量池(Constant Pool)的MethodRef常量已經(jīng)固定了目標(biāo)方法的符號信息(方法所屬者及其類型,方法名字、參數(shù)順序和類型、返回值)。虛擬機(jī)使用符號信息能直接解釋出具體的方法,直接調(diào)用。
  • 而invokedynamic指令在編譯期間生成的class文件中,對應(yīng)常量池(Constant Pool)的Invokedynamic_Info常量存儲的符號信息中并沒有方法所屬者及其類型 ,替代的是BootstapMethod信息。在運(yùn)行時(shí),通過引導(dǎo)方法BootstrapMethod機(jī)制動態(tài)確定方法的所屬者和類型。這一特點(diǎn)也非常契合動態(tài)類型語言只有在運(yùn)行期間才能確定類型的特征。

那么,invokedynamic如何通過引導(dǎo)方法找到所屬者及其類型?我們依然結(jié)合前面的J8Sample實(shí)例:

  1. javap -v J8Sample.class  

結(jié)合J8Sample.class字節(jié)碼,并對invokedynamic指令調(diào)用過程進(jìn)行跟蹤分析??偨Y(jié)如下:

 

 

依據(jù)上圖invokedynamic調(diào)用步驟,我們一步一步做一個(gè)分析講解。

步驟1 選取J8Sample.java源碼中Lambda表達(dá)式1:

Runnable runnable = () -> System.out.println("xixi"); // lambda表達(dá)式1

步驟2 通過javac J8Sample.java編譯得到J8Sample.class之后,

Lambda表達(dá)式1變成:0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;

對應(yīng)在J8Sample.class中發(fā)現(xiàn)了新增的私有靜態(tài)方法:


步驟3 針對表達(dá)式1的字節(jié)碼分析 #2 對應(yīng)的是class文件中的常量池:

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

注意,這里InvokeDynamic不是指令,代表的是Constant_InvokeDynamic_Info結(jié)構(gòu)。

步驟4 結(jié)構(gòu)后面緊跟的 #0 標(biāo)識的是class文件中的BootstrapMethod區(qū)域中引導(dǎo)方法的索引:


步驟5 引導(dǎo)方法中的 java/lang/invoke/LambdaMetafactory.metafactory才是invokedynamic指令的關(guān)鍵:


該方法會在運(yùn)行時(shí),在內(nèi)存中動態(tài)生成一個(gè)實(shí)現(xiàn)Lambda表達(dá)式對應(yīng)函數(shù)式接口的實(shí)例類型,并在接口的實(shí)現(xiàn)方法中調(diào)用步驟2中新增的靜態(tài)私有方法。

步驟6 使用java -Djdk.internal.lambda.dumpProxyClasses J8Sample.class運(yùn)行一下,可以內(nèi)存中動態(tài)生成的類型輸出到本地:


步驟7 通過javap -p -c J8Sample\$\$Lambda\$1.class反編譯一下,可以看到生成類的實(shí)現(xiàn):

 

在run方法中使用了invokestatic指令,直接調(diào)用了J8Sample.lambda$main$0這個(gè)在編譯期間生成的靜態(tài)私有方法。

至此,上面7個(gè)步驟就是Lambda表達(dá)式在Java的底層的實(shí)現(xiàn)原理。Android 針對這些實(shí)現(xiàn)會怎么處理呢?

Android不能直接支持

回到Android系統(tǒng)上,Java-Bytecode(JVM字節(jié)碼)是不能直接運(yùn)行在Android系統(tǒng)上的,需要轉(zhuǎn)換成Android-Bytecode(Dalvik/ART 字節(jié)碼)。

如圖:

 

 

通過Lambda這節(jié),我們知道Java底層是通過invokedynamic指令來實(shí)現(xiàn),由于Dalvik/ART并沒有支持invokedynamic指令或者對應(yīng)的替代功能。簡單的來說,就是Android的dex編譯器不支持invokedynamic指令,導(dǎo)致Android不能直接支持Java 8。

Android間接支持

既然不能直接支持,那就只能在Java-Bytecode轉(zhuǎn)換到Android-Bytecode這一過程中想辦法,間接支持。這個(gè)間接支持的過程我們統(tǒng)稱為Desugar(脫糖)過程。

官方流程圖:


當(dāng)前,無論是RetroLambda,還是Google的Jack & Jill 工具,還是最新的D8 dex編譯器:

  • 流程方面:都是按照如上圖所示的官方流程進(jìn)行Desugar的。
  • 原理方面:卻是參照Lambda在Java底層的實(shí)現(xiàn),并將這些實(shí)現(xiàn)移至到RetroLambda插件或者Jack、D8編譯器工具中。

下面我們逐個(gè)分析解讀一下。

Android 間接支持之RetroLambda

 

 

如圖所示,RetroLambda 的Desugar過程發(fā)生在javac將源碼編譯完成之后,dx工具進(jìn)行dex編譯之前。

RetroLambda Desugar

其實(shí)就是參照invokedynamic指令解讀一節(jié)中的步驟5,根據(jù)java/lang/invoke/LambdaMetafactory.metafactory方法,直接將原本在運(yùn)行時(shí)生成在內(nèi)存中的J8Sample\$\$Lambda\$1.class,在javac編譯結(jié)束之后,dx編譯dex之前,直接生成到本地,并使用生成的J8Sample\$\$Lambda\$1類修改J8Sample.class字節(jié)碼文件,將J8Sample.class中的invokedynamic指令替換成invokestatic指令。

將實(shí)例中的J8Sample.java放到一個(gè)配置了Retrolambda的Android工程中:

 

 

AndroidStudio -> Build -> make project 編譯之后:

 

 

app:transformClassesWithRetrolambdaForDebug任務(wù)發(fā)生在app:compileDebugJavaWithJavac(對應(yīng)javac)之后,和app:transformDexArchiveWithDexMergerForDebug(對應(yīng)dx)之前,同時(shí)在build/intermediates/transforms/retrolambda下面生產(chǎn)如圖所示的class文件。

J8Sample.class和J8Sample$$Lambda$1.class反編譯之后的代碼如下:

 

 

通過反編譯代碼,可以看出J8Sample.class中Lambda表達(dá)式已經(jīng)被我們熟悉的1.7or1.6的語句所替代。

注意:右圖中J8Sample.lambda$main$0()方法在左圖中沒有顯示出來,但是J8Sample.class字節(jié)碼確實(shí)是存在的。

Android間接支持之Jack&Jill工具

 

 

Jack是基于Eclipse的ecj編譯開發(fā)的, Jill是基于ASM4開發(fā)的。Jack&Jill工具鏈?zhǔn)荊oogle在Android N(7.0)發(fā)布的,用于替換javac&dx的工具鏈,并且在jack過程內(nèi)置了Desugar過程。

但是在Android P(9.0) 的時(shí)候?qū)ack&Jill工具鏈廢棄了,被 javac&D8工具鏈替代了。這里就不做Desugar具體分析了。


D8是Android P(9.0)新增的dex編譯器。并在Android Studio 3.1版本中默認(rèn)使用D8作為dex的默認(rèn)編譯器。

D8 Desugar

如圖所示,Desugar過程放在了D8的內(nèi)部,由Android Studio這個(gè)IDE來實(shí)現(xiàn)這個(gè)轉(zhuǎn)換,原理基本和RetroLambda是一樣。

本質(zhì)上也是參照java/lang/invoke/LambdaMetafactory.metafactory方法直接將原本在運(yùn)行時(shí)生成在內(nèi)存中的J8Sample\$\$Lambda\$1.class,在D8的編譯dex期間,直接生成并寫入到dex文件中。

同樣,將實(shí)例中的J8Sample.java放到支持D8的Android工程中:


同樣,AndroidStudio -> Build -> make project編譯之后:


javac編譯之后的J8Sample.class還是使用invokedynamic指令,即這一步并沒有Desugar:


在app:transformDexArchiveWithDexMergerForDebug(對應(yīng)dx)任務(wù)之后,再對應(yīng)build/intermediates/transforms/dexMerger目錄找第0個(gè)classex.dex。

執(zhí)行$ANDROID_HOME/build-tools/28.0.3/dexdump -d classes.dex >> dexInfo.txt拿到dex信息。

還是選取實(shí)例中Lambda表達(dá)式1 :Runnable runnable = () -> System.out.println("xixi");來進(jìn)行分析。

這個(gè)dexIno.txt文件非常大,有1.4M,我們通過com.J8Smaple2.J8Sample找到我們J8Sample在dex中位置。

 

新增方法:


J8Sample.main方法:


圖中選中部分,對應(yīng)就是Lambda表達(dá)式1 desugar之后的內(nèi)容。

翻譯成Java的話就變成了:new Lcom/j8sample2/-$$Lambda$J8Sample$jWmuYH0zEF070TKXrjBFgnnqOKc這個(gè)生成類的一個(gè)對象。

類Lcom/j8sample2/-$$Lambda$J8Sample$jWmuYH0zEF070TKXrjBFgnnqOKc對應(yīng)前面的生成的J8Sample$$Lambda$1類型,只不過數(shù)字1變成了Hash值。


實(shí)現(xiàn)Interface Ljava/lang/Runnable。 Lcom/j8sample2/-$$Lambda$J8Sample$jWmuYH0zEF070TKXrjBFgnnqOKc.run方法:


到這里,是不是和前面RetroLambda就一樣了。

總結(jié)

至此,Lambda及其invokedynamic指令、RetroLambda插件、D8編譯器各自的原理分析都已經(jīng)結(jié)束了。

相比較Lambda在Java8自己內(nèi)部的實(shí)現(xiàn):即運(yùn)行時(shí),在內(nèi)存中動態(tài)生成關(guān)聯(lián)的函數(shù)式接口的實(shí)例類型,通過BSM-引導(dǎo)方法找到該內(nèi)存類(字節(jié)碼層面的反射)。

在Android上的其他三種Desugar方式,原理都是一樣的,區(qū)別在于時(shí)機(jī)不同:

RetroLambda將函數(shù)式接口對應(yīng)的實(shí)例類型的生產(chǎn)過程,放在javac編譯之后,dx編譯之前,并動態(tài)修改了表達(dá)式所屬的字節(jié)碼文件。

Jack&Jill是直接將接口對應(yīng)的實(shí)例類型,直接jack過程中生成,并編譯進(jìn)了dex文件。

D8的過程是在dex編譯過程中,直接在內(nèi)存生成接口對應(yīng)的實(shí)例類型,并將生成的類型直接寫入生成的dex文件中。

探討

無論是RetroLambda,還是D8,對Java8的特性也不是全都支持。

Java8新增的許多API(例如:新的DataAPI),就D8編譯器而言,只有在Android P(9.0)版本中能直接運(yùn)行。低于9.0就不行了。如何能夠全版本支持Java 8。D8還有很長的一段路要走。

如果我們在低版本需要使用新的API,目前可以采取將這些API打包進(jìn)去的臨時(shí)辦法。

寫到這里,肯定有人要提出,為什么不直接使用Kotlin呢?確實(shí)Kotlin對Lambda表達(dá)式、函數(shù)引用等特性都做了很好的支持,但是現(xiàn)實(shí)的情況中,Kotlin很難取代Android中的Java。新業(yè)務(wù)、新工程還相對容易,對老業(yè)務(wù)來說,尤其是經(jīng)過多年沉淀,工程結(jié)構(gòu)復(fù)雜,遷移改造帶來的收益,往往遠(yuǎn)遠(yuǎn)小于遷移改造帶來的成本和不可控之風(fēng)險(xiǎn)。Kotlin和Java同時(shí)存在的情況,長期來看是一個(gè)必然的結(jié)果。

至于Java 8的其他特性呢,D8是如何實(shí)現(xiàn)的,也可以按照上面類似的方式去分析,甚至可以結(jié)合Kotlin實(shí)現(xiàn)的方式,一探究竟。

 

責(zé)任編輯:武曉燕 來源: 51CTO專欄
相關(guān)推薦

2021-02-22 11:51:15

Java開發(fā)代碼

2023-10-10 08:39:25

Java 7Java 8

2014-04-16 07:43:31

Java 8JRE

2014-05-05 09:58:01

2016-09-12 14:33:20

javaHashMap

2013-05-02 09:14:19

Java 8Java 8的新特性

2014-10-20 13:57:59

JavaFX 8Java 8

2014-07-15 14:12:17

Java8

2015-11-02 10:13:41

iOSObjective-C語法

2014-07-15 14:48:26

Java8

2023-02-07 09:17:19

Java注解原理

2013-11-05 09:47:12

Android 4.4特性

2013-04-09 12:59:21

WindowsPhon

2017-10-25 11:05:14

Java

2010-09-15 17:05:33

CSS display

2015-06-15 10:12:36

Java原理分析

2014-03-19 11:04:14

Java 8Java8特性

2014-04-15 15:45:22

Java8Java8教程

2011-06-16 08:43:39

JAVAJIN

2017-04-12 10:02:21

Java阻塞隊(duì)列原理分析
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號