Lambda in Android

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ù)污淋。

android 構(gòu)建流程圖

原圖見(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ù)方法引用

可參考 Translation of Lambda Expressions哀澈。

原文鏈接:https://dourok.info/2017/10/20/lambda-in-android/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市度气,隨后出現(xiàn)的幾起案子割按,更是在濱河造成了極大的恐慌,老刑警劉巖磷籍,帶你破解...
    沈念sama閱讀 222,590評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件适荣,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡院领,警方通過(guò)查閱死者的電腦和手機(jī)弛矛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)比然,“玉大人丈氓,你說(shuō)我怎么就攤上這事∏糠ǎ” “怎么了万俗?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,301評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)拟烫。 經(jīng)常有香客問(wèn)我该编,道長(zhǎng)迄本,這世上最難降的妖魔是什么硕淑? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,078評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮嘉赎,結(jié)果婚禮上置媳,老公的妹妹穿的比我還像新娘。我一直安慰自己公条,他們只是感情好拇囊,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著靶橱,像睡著了一般寥袭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上关霸,一...
    開(kāi)封第一講書(shū)人閱讀 52,682評(píng)論 1 312
  • 那天传黄,我揣著相機(jī)與錄音,去河邊找鬼队寇。 笑死膘掰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的佳遣。 我是一名探鬼主播识埋,決...
    沈念sama閱讀 41,155評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼凡伊,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了窒舟?” 一聲冷哼從身側(cè)響起系忙,我...
    開(kāi)封第一講書(shū)人閱讀 40,098評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惠豺,沒(méi)想到半個(gè)月后笨觅,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,638評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡耕腾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評(píng)論 3 342
  • 正文 我和宋清朗相戀三年见剩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扫俺。...
    茶點(diǎn)故事閱讀 40,852評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡苍苞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出狼纬,到底是詐尸還是另有隱情羹呵,我是刑警寧澤,帶...
    沈念sama閱讀 36,520評(píng)論 5 351
  • 正文 年R本政府宣布疗琉,位于F島的核電站冈欢,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏盈简。R本人自食惡果不足惜凑耻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望柠贤。 院中可真熱鬧香浩,春花似錦、人聲如沸臼勉。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,674評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)宴霸。三九已至囱晴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瓢谢,已是汗流浹背畸写。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,788評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留恩闻,地道東北人艺糜。 一個(gè)月前我還...
    沈念sama閱讀 49,279評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親破停。 傳聞我的和親對(duì)象是個(gè)殘疾皇子翅楼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 本文是對(duì) Brian Goetz 的 State of Lambda 一文的翻譯 為什么要翻譯這個(gè)系列黑界? andr...
    aaron688閱讀 3,892評(píng)論 4 31
  • lambda表達(dá)式(又被成為“閉包”或“匿名方法”)方法引用和構(gòu)造方法引用擴(kuò)展的目標(biāo)類(lèi)型和類(lèi)型推導(dǎo)接口中的默認(rèn)方法...
    183207efd207閱讀 1,488評(píng)論 0 5
  • 注:之前關(guān)于Java8的認(rèn)知一直停留在知道有哪些修改和新的API上,對(duì)Lambda的認(rèn)識(shí)也是僅僅限于對(duì)匿名內(nèi)部類(lèi)的...
    mualex閱讀 2,826評(píng)論 1 4
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理管嬉,服務(wù)發(fā)現(xiàn)怯伊,斷路器几颜,智...
    卡卡羅2017閱讀 134,715評(píng)論 18 139
  • 2016年的第一時(shí)刻愿大家平安 快樂(lè)@我愛(ài)的你們?
    向日葵dekyi閱讀 167評(píng)論 0 1