Android 如何實(shí)現(xiàn)支持 lambda 表達(dá)式
lambda 表達(dá)式是 java 8 新引入的語(yǔ)言特性,使用了通過(guò) java 7 新引入的字節(jié)碼指令 invokedynamic 來(lái)實(shí)現(xiàn)的(參考 Goetz-jvmls-lambda.pdf)芹缔。但在 dalvik 中并沒(méi)有相應(yīng)的指令昆稿,所以直接將 java 8 的字節(jié)碼翻譯為 dalvik 字節(jié)碼目前是是不可行的匪凡。不過(guò)從 java lambda 的實(shí)現(xiàn)上來(lái)講,實(shí)際上就是內(nèi)部匿名類(lèi)的語(yǔ)法糖。
既然是語(yǔ)法糖,那就是一個(gè)代碼轉(zhuǎn)換的事殊轴,把這個(gè)過(guò)程抽離出來(lái)另外實(shí)現(xiàn),就可以在低版本的 jdk 中實(shí)現(xiàn)對(duì) lambda 的支持袒炉。retrolambda旁理,就是在字節(jié)碼層面實(shí)現(xiàn)這個(gè)轉(zhuǎn)換。retrolambda 的具體實(shí)現(xiàn)是基于 java 8 對(duì) lambda 的底層實(shí)現(xiàn)來(lái)做的我磁。在編譯時(shí)孽文,java 主要為當(dāng)前類(lèi)(lambda 表達(dá)式所在的類(lèi))生成一個(gè)方法驻襟,方法體(method body)就是 lambda body,這個(gè)方法稱(chēng)為 desugar 方法芋哭。運(yùn)行時(shí)沉衣,第一次執(zhí)行到這條 lambda 語(yǔ)句的時(shí)候,invokedynamic 調(diào)用引導(dǎo)方法(BSM)楷掉,引導(dǎo)方法生成一個(gè)實(shí)現(xiàn)了具體函數(shù)式接口(Functional Interface厢蒜,只有一個(gè)抽象方法的接口)的 VM 匿名類(lèi)霞势,這個(gè)類(lèi)主要用于捕獲 lambda 所需要的變量烹植。第二步,把這個(gè)對(duì)象的構(gòu)造函數(shù)和 invokdynamic 綁定起來(lái)愕贡,最后調(diào)用這個(gè)構(gòu)造函數(shù)返回這個(gè)匿名類(lèi)的實(shí)例草雕,也就是所謂的 lambda object(以后再執(zhí)行這條 invokedynamic 指令就是直接調(diào)用構(gòu)造函數(shù)返回實(shí)例了)。調(diào)用的時(shí)候固以,再把接口方法需要的參數(shù)和捕獲的變量傳遞給 desugar 方法來(lái)完成 lambda 的應(yīng)用(可參考理解 invokedynamic)墩虹。
retrolambda 的做法是,源文件先用 java 8 編譯憨琳,lambda body 轉(zhuǎn)換為當(dāng)前類(lèi)的 desugar 方法編譯器已經(jīng)處理好了诫钓。接著解析編譯后的 class 文件,遇到一條 invokedynamic 指令篙螟,就模仿它調(diào)用它的引導(dǎo)方法(LambdaReifier.reifyLambdaClass)菌湃,把引導(dǎo)方法生成的匿名類(lèi)作為當(dāng)前類(lèi)的匿名類(lèi)保存下來(lái),接下來(lái)還會(huì)對(duì)這些類(lèi)再做一些變換遍略,包括用單例優(yōu)化無(wú)狀態(tài)的 lambda 對(duì)象惧所,將構(gòu)造函數(shù)替換為工廠方法(BackportLambdaClass#visitEnd)。最后把 invokedynamic 替換為對(duì)該匿名類(lèi)的實(shí)例化語(yǔ)句绪杏,就是這樣把 invokedynamic 替換為等價(jià)的兼容代碼下愈。不過(guò), retrolambda 的實(shí)現(xiàn)依賴(lài)于 java 對(duì) lambda 的具體實(shí)現(xiàn)蕾久,后續(xù)的 java 版本不用匿名類(lèi)了势似,那么 retrolambda 也就不能用了。
在 Android Studio 3.0 之前僧著,要在基于 java 的 Android 開(kāi)發(fā)中使用 lambda 表達(dá)一般都是用 retrolambda 來(lái)轉(zhuǎn)換為 dex 能處理的字節(jié)碼來(lái)實(shí)現(xiàn)的(就不提夭折的 Jack 了)叫编。 不過(guò) Android Studio 3.0 后,IDE 已經(jīng)支持實(shí)現(xiàn)這個(gè)轉(zhuǎn)換了霹抛,簡(jiǎn)稱(chēng) desugar搓逾。具體如何開(kāi)啟可參看官方文檔:Use Java 8 language features。IDE 的 desugar 過(guò)程比 retrolamda 的主要區(qū)別就是時(shí)機(jī)不同杯拐,原理上大致是一樣的霞篡,IDE 的實(shí)現(xiàn)可見(jiàn) LambdaDesugaring#visitInvokeDynamicInsn世蔗。 retrolambda 只能對(duì)當(dāng)前項(xiàng)目進(jìn)行轉(zhuǎn)換,IDE 是在轉(zhuǎn)換為 dex 之前做的轉(zhuǎn)換朗兵,也就是說(shuō) IDE 還支持第三方用 java 8 編譯的庫(kù)污淋。
原圖見(jiàn) Build Workflow - Android Studio Project Site
總之,Android 對(duì) lambda 的實(shí)現(xiàn)與 java 8 并未太大區(qū)別余掖,最主要的區(qū)別 java 8 的匿名類(lèi)在運(yùn)行時(shí)生成寸爆,而 Android 是在編譯時(shí)生成(這樣還可以避免了對(duì) serializable lambda 的特殊對(duì)待)。
lambda 表達(dá)式
lambda 表達(dá)式在 java 中就是用于創(chuàng)建函數(shù)式接口實(shí)例(lambda object)的表達(dá)式盐欺,lambda 的實(shí)際使用中赁豆,主要將其分為兩種類(lèi)型,其一冗美,無(wú)狀態(tài)的(stateless) lambda 表達(dá)式魔种,指的就是沒(méi)有自由變量的 lambda 表達(dá)式。相對(duì)的粉洼,另一類(lèi)就是有自由變量的 lambda 表達(dá)式节预。
什么是自由變量,把一道 lambda 表達(dá)式從其上下文抽離出來(lái)看一下:L1 = s -> Integer.valueOf(s)
属韧。表達(dá)式中的兩個(gè)量 Integer 和 s安拟,Integer 是常量,而 s 在參數(shù)列表中聲明了(類(lèi)型省略)宵喂,這里稱(chēng) s 是一個(gè)綁定變量糠赦,所有量都是確定的,所以 L1 就是無(wú)狀態(tài)的 lambda 表達(dá)式(可以認(rèn)為它的調(diào)用不會(huì)產(chǎn)生任何副作用)樊破。
另外一個(gè)例子:() -> System.out.println(Arrays.toString(args))
愉棱。args
是什么?脫離了上下文就無(wú)法確定了哲戚,如果在上下文中看奔滑,就很清楚 args
是什么了:
public static void main(String[] args) {
Runnable r = () -> System.out.println(Arrays.toString(args));
r.run();
}
args
在這里就是自由變量。要對(duì) lambda 表達(dá)式求值前所有自由變量都是得已知的顺少,java 中所有自由變量都必須在編譯期確認(rèn)(另外一種不同的實(shí)現(xiàn)可參考 Groovy)朋其,為自由變量確定值的過(guò)程稱(chēng)為變量捕獲(capturing),把變量捕獲后和 lambda 表達(dá)式綁定在一起的結(jié)構(gòu)就是閉包(closure)脆炎,lambda 對(duì)象實(shí)例就是一個(gè)閉包梅猿。java 中就是通過(guò)匿名類(lèi)來(lái)存放這些捕獲這些變量,而且是以 final 引用的形式秒裕,所以更應(yīng)該說(shuō)是值而不是變量袱蚓。
先看一下最簡(jiǎn)單的無(wú)狀態(tài) lambda:
public class LambdaTest {
public void testStateless() {
Runnable r = (() -> System.out.println("pure"));
r.run();
}
}
編譯后再反編譯,可以看到几蜻,變成了兩個(gè)類(lèi)(可以在 build/intermediates/transforms/desugar
中找到):
LambdaTest:
public class LambdaTest {
public void testStateless() {
Runnable r = LambdaTest$$Lambda$0.$instance;
r.run();
}
static void lambda$testPure$0$LambdaTest(){
System.out.println("pure");
}
}
LambdaTest$$Lambda$0:
final class LambdaTest$$Lambda$0 implements Runnable {
static final Runnable $instance = new LambdaTest$$Lambda$0();
private LambdaTest$$Lambda$0() {
}
public void run() {
LambdaTest.lambda$testPure$0$LambdaTest();
}
}
lambda body 變成了 LambdaTest 中的一個(gè)靜態(tài)方法喇潘,也就是所謂的 desugar 方法体斩,另外還生成了一個(gè)類(lèi) LambdaTest$$Lambda$0
實(shí)現(xiàn)了函數(shù)式接口,在其實(shí)現(xiàn)方法里再去調(diào)用 desugar 方法颖低,無(wú)狀態(tài) lambda 對(duì)象不需要保存額外的參數(shù)絮吵,這里用單例進(jìn)行優(yōu)化。
如果捕獲了變量忱屑,以局部變量和形式參數(shù)為例蹬敲,無(wú)論是局部變量還是上下文方法的形式參數(shù),它們的值和類(lèi)型都是編譯時(shí)確定的:
public void capturingLocal(String strp) {
String str = "lexical";
Runnable r = () -> System.out.println(str + strp);
r.run();
}
LambdaTest$$Lambda$1:
final class LambdaTest$$Lambda$1 implements Runnable {
private final String arg$1;
private final String arg$2;
LambdaTest$$Lambda$1(String var1, String var2) {
this.arg$1 = var1;
this.arg$2 = var2;
}
public void run() {
LambdaTest.lambda$capturingLocal$1$LambdaTest(this.arg$1, this.arg$2);
}
}
原先的 lambda 表達(dá)式賦值語(yǔ)句變成了 Runnable r = new LambdaTest$$Lambda$1(str, strp)
莺戒,自由變量都通過(guò) lambda 對(duì)象構(gòu)造器進(jìn)行捕獲并保存起來(lái)伴嗡,對(duì) lambda 求值的時(shí)候再傳遞給 desugar 方法,這里 Runnable 的方法沒(méi)有形式參數(shù)脏毯,如果有形式參數(shù)的話(huà)闹究,這些捕獲的變量會(huì)排在形式參數(shù)后面再傳遞給 desugar 方法幔崖。
如果在 lambda 中引用了對(duì)象字段:
private String stri = "instance";
public void capturingInstance() {
Runnable r = () -> System.out.println(stri);
r.run();
}
LambdaTest$$Lambda$4:
final class LambdaTest$$Lambda$4 implements Runnable {
private final LambdaTest arg$1;
LambdaTest$$Lambda$4(LambdaTest var1) {
this.arg$1 = var1;
}
public void run() {
this.arg$1.lambda$capturingInstance$4$LambdaTest();
}
}
可以看到 lambda 對(duì)象保存了上下文類(lèi)的引用食店,無(wú)論是實(shí)例變量還是實(shí)例方法,實(shí)際上都有一個(gè)隱性的接收者就是 this
赏寇,當(dāng)然也可以顯性的聲明吉嫩,在 lambda body 中的 this
引用指向的就是其上下文的類(lèi),而不是 lambda 對(duì)象(與匿名類(lèi)的區(qū)別)嗅定。在這里 lambda 表達(dá)捕獲的變量就是實(shí)例變量的接收者 this
而不是實(shí)例變量本身自娩。而且可以看到 lambda 的 desugar 方法變成了實(shí)例方法,用這種方式渠退,lambda body 幾乎不用做任何轉(zhuǎn)換只需照搬進(jìn)方法體就行忙迁。還包括對(duì) super
的處理,lambda 對(duì)象無(wú)法捕獲 super碎乃,只能通過(guò)調(diào)用 this 的實(shí)例方法來(lái)實(shí)現(xiàn)對(duì) super 的調(diào)用姊扔,可見(jiàn)用 desugar 方法來(lái)實(shí)現(xiàn)是十分便利的。
this
的捕獲梅誓,對(duì)于 Android 開(kāi)發(fā)來(lái)說(shuō)特別要注意恰梢,在 Activity 中使用 lambda 表達(dá)式的話(huà),意味著會(huì)通過(guò) final 引用的形象將當(dāng)前 Activity 實(shí)例傳遞到外部去梗掰,稍不注意便會(huì)引起泄露嵌言。一個(gè)顯而易見(jiàn)的技巧,將實(shí)例字段賦值給局部變量及穗,就不會(huì)捕獲 this 引用了摧茴。當(dāng)然對(duì)于生命周期相關(guān)的對(duì)象來(lái)說(shuō)還是不安全的,比如 View埂陆。
方法引用
方法引用基本可以當(dāng)成是 lambda 表達(dá)式的一個(gè)特例苛白,方法引用都可以用相應(yīng)的 lambda 表達(dá)式來(lái)代替尘分,有一個(gè)例外就是帶有類(lèi)型參數(shù)方法的函數(shù)式接口,能用方法引用但不能用 lambda 表達(dá)式丸氛,見(jiàn) java - Lambda Expression and generic method - Stack Overflow培愁。方法引用也分為捕獲與非捕獲,對(duì)于無(wú)須捕獲接的方法引用主要有:
- 靜態(tài)方法
- 構(gòu)造器
- 未綁定的實(shí)例方法
什么是未綁定的實(shí)例方法缓窜?方法引用語(yǔ)法可以大致認(rèn)為是接收者::方法名
這樣的形式定续,方法可以是實(shí)例方法或者是靜態(tài)方法,當(dāng)方法是實(shí)例方法而接收者是類(lèi)引用時(shí)禾锤,這時(shí)接收者就是一個(gè)未綁定的接收者:
list.filter(String::isEmpty)
isEmpty
是實(shí)例方法私股,而接收者是類(lèi)引用,在這里接收者在運(yùn)行會(huì)被替換為被替換為 list 內(nèi)的元素恩掷,等價(jià)于這樣的 lambda 表達(dá)式:
list.filter(s -> s.isEmpty())
注意非綁定的實(shí)例方法引用是有二義性的倡鲸,java 根據(jù)方法的聲明去推定 isEmpty
是實(shí)例方法還是靜態(tài)方法,以下面的類(lèi)為例:
public class C{
public static boolean isEmpty(C c);
public boolean isEmpty();
}
如上面的方法聲明兩個(gè)方法對(duì)于表達(dá)式 list.filter(C::isEmpty)
來(lái)說(shuō)都是合法的黄娘,java 也就無(wú)法推斷出這里是指哪個(gè)方法引用峭状,所以編譯器報(bào)錯(cuò)。
需要捕獲的方法引用逼争,也就是已綁定實(shí)例的方法引用优床,包括實(shí)例方法,內(nèi)部類(lèi)(數(shù)組)的構(gòu)造器誓焦,super 方法胆敞。接收者就是閉包所要捕獲的變量。但要注意一點(diǎn)方法引用是沒(méi)有隱式聲明的 this
引用的杂伟。比如下面兩個(gè)方法移层,從語(yǔ)義上來(lái)說(shuō)是等價(jià)的,
public void capturingInstance() {
Predicate<String> c = s -> stri.equals(s);
}
public void capturingIntanceMethod() {
Predicate<String> c = stri::equals;
}
但是他們捕獲的引用卻不一樣赫粥,上文可知 lambda 表達(dá)式捕獲的是隱式聲明的 this
观话,而方法引用捕獲的卻是直接接收者:
final class LambdaTest$$Lambda$8 implements Predicate {
private final String arg$1;
private LambdaTest$$Lambda$8(String var1) {
this.arg$1 = var1;
}
static Predicate get$Lambda(String var0) {
return new LambdaTest$$Lambda$8(var0);
}
public boolean test(Object var1) {
return this.arg$1.equals((String)var1);
}
}
還有一點(diǎn),使用方法引用傅是,因?yàn)榉椒ㄒ呀?jīng)是現(xiàn)成的匪燕,大部分情況就沒(méi)必要重新生成一個(gè) desugar 方法。
但有例外喧笔,super 和可變參數(shù)帽驯,需要一個(gè)橋接方法。對(duì)于 super 來(lái)說(shuō)书闸,lambda 對(duì)象是無(wú)法不會(huì)當(dāng)前類(lèi)的 super 引用的尼变,所以需要借由當(dāng)前類(lèi)的實(shí)例方法來(lái)實(shí)現(xiàn)對(duì) super 的引用。
接收者也可以是表達(dá)式:
Predicate<String> c = (stri.equals("abc") ? "abc" : "bcd")::equals;
在這里捕獲的是表達(dá)式求值的結(jié)果而不是表達(dá)式。
所以對(duì)于 Activity 來(lái)說(shuō)嫌术,要格外注意下面幾種情況可能導(dǎo)致引用泄露:
-
this
關(guān)鍵字的方法引用 -
super
關(guān)鍵字的方法引用 - 非靜態(tài)內(nèi)部類(lèi)的構(gòu)造器引用
- Activity 或其實(shí)例變量可變參數(shù)方法引用