Lambda in Android

Android 如何實現支持 lambda 表達式

lambda 表達式是 java 8 新引入的語言特性,使用了通過 java 7 新引入的字節碼指令 invokedynamic 來實現的(參考 Goetz-jvmls-lambda.pdf)。但在 dalvik 中并沒有相應的指令,所以直接將 java 8 的字節碼翻譯為 dalvik 字節碼目前是是不可行的。不過從 java lambda 的實現上來講,實際上就是內部匿名類的語法糖。

既然是語法糖,那就是一個代碼轉換的事,把這個過程抽離出來另外實現,就可以在低版本的 jdk 中實現對 lambda 的支持。retrolambda,就是在字節碼層面實現這個轉換。retrolambda 的具體實現是基于 java 8 對 lambda 的底層實現來做的。在編譯時,java 主要為當前類(lambda 表達式所在的類)生成一個方法,方法體(method body)就是 lambda body,這個方法稱為 desugar 方法。運行時,第一次執行到這條 lambda 語句的時候,invokedynamic 調用引導方法(BSM),引導方法生成一個實現了具體函數式接口(Functional Interface,只有一個抽象方法的接口)的 VM 匿名類,這個類主要用于捕獲 lambda 所需要的變量。第二步,把這個對象的構造函數和 invokdynamic 綁定起來,最后調用這個構造函數返回這個匿名類的實例,也就是所謂的 lambda object(以后再執行這條 invokedynamic 指令就是直接調用構造函數返回實例了)。調用的時候,再把接口方法需要的參數和捕獲的變量傳遞給 desugar 方法來完成 lambda 的應用(可參考理解 invokedynamic)。

retrolambda 的做法是,源文件先用 java 8 編譯,lambda body 轉換為當前類的 desugar 方法編譯器已經處理好了。接著解析編譯后的 class 文件,遇到一條 invokedynamic 指令,就模仿它調用它的引導方法(LambdaReifier.reifyLambdaClass),把引導方法生成的匿名類作為當前類的匿名類保存下來,接下來還會對這些類再做一些變換,包括用單例優化無狀態的 lambda 對象,將構造函數替換為工廠方法(BackportLambdaClass#visitEnd)。最后把 invokedynamic 替換為對該匿名類的實例化語句,就是這樣把 invokedynamic 替換為等價的兼容代碼。不過, retrolambda 的實現依賴于 java 對 lambda 的具體實現,后續的 java 版本不用匿名類了,那么 retrolambda 也就不能用了。

在 Android Studio 3.0 之前,要在基于 java 的 Android 開發中使用 lambda 表達一般都是用 retrolambda 來轉換為 dex 能處理的字節碼來實現的(就不提夭折的 Jack 了)。 不過 Android Studio 3.0 后,IDE 已經支持實現這個轉換了,簡稱 desugar。具體如何開啟可參看官方文檔:Use Java 8 language features。IDE 的 desugar 過程比 retrolamda 的主要區別就是時機不同,原理上大致是一樣的,IDE 的實現可見 LambdaDesugaring#visitInvokeDynamicInsn。 retrolambda 只能對當前項目進行轉換,IDE 是在轉換為 dex 之前做的轉換,也就是說 IDE 還支持第三方用 java 8 編譯的庫。

android 構建流程圖

原圖見 Build Workflow - Android Studio Project Site

總之,Android 對 lambda 的實現與 java 8 并未太大區別,最主要的區別 java 8 的匿名類在運行時生成,而 Android 是在編譯時生成(這樣還可以避免了對 serializable lambda 的特殊對待)。

lambda 表達式

lambda 表達式在 java 中就是用于創建函數式接口實例(lambda object)的表達式,lambda 的實際使用中,主要將其分為兩種類型,其一,無狀態的(stateless) lambda 表達式,指的就是沒有自由變量的 lambda 表達式。相對的,另一類就是有自由變量的 lambda 表達式。

什么是自由變量,把一道 lambda 表達式從其上下文抽離出來看一下:L1 = s -> Integer.valueOf(s)。表達式中的兩個量 Integer 和 s,Integer 是常量,而 s 在參數列表中聲明了(類型省略),這里稱 s 是一個綁定變量,所有量都是確定的,所以 L1 就是無狀態的 lambda 表達式(可以認為它的調用不會產生任何副作用)。

另外一個例子:() -> System.out.println(Arrays.toString(args))args 是什么?脫離了上下文就無法確定了,如果在上下文中看,就很清楚 args 是什么了:

public static void main(String[] args) {
    Runnable r = () -> System.out.println(Arrays.toString(args));
    r.run();
}

args 在這里就是自由變量。要對 lambda 表達式求值前所有自由變量都是得已知的,java 中所有自由變量都必須在編譯期確認(另外一種不同的實現可參考 Groovy),為自由變量確定值的過程稱為變量捕獲(capturing),把變量捕獲后和 lambda 表達式綁定在一起的結構就是閉包(closure),lambda 對象實例就是一個閉包。java 中就是通過匿名類來存放這些捕獲這些變量,而且是以 final 引用的形式,所以更應該說是值而不是變量。

先看一下最簡單的無狀態 lambda:

public class LambdaTest {
    public void testStateless() {
        Runnable r = (() -> System.out.println("pure"));
        r.run();
    }
}

編譯后再反編譯,可以看到,變成了兩個類(可以在 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 中的一個靜態方法,也就是所謂的 desugar 方法,另外還生成了一個類 LambdaTest$$Lambda$0 實現了函數式接口,在其實現方法里再去調用 desugar 方法,無狀態 lambda 對象不需要保存額外的參數,這里用單例進行優化。

如果捕獲了變量,以局部變量和形式參數為例,無論是局部變量還是上下文方法的形式參數,它們的值和類型都是編譯時確定的:

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 表達式賦值語句變成了 Runnable r = new LambdaTest$$Lambda$1(str, strp),自由變量都通過 lambda 對象構造器進行捕獲并保存起來,對 lambda 求值的時候再傳遞給 desugar 方法,這里 Runnable 的方法沒有形式參數,如果有形式參數的話,這些捕獲的變量會排在形式參數后面再傳遞給 desugar 方法。

如果在 lambda 中引用了對象字段:

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 對象保存了上下文類的引用,無論是實例變量還是實例方法,實際上都有一個隱性的接收者就是 this,當然也可以顯性的聲明,在 lambda body 中的 this 引用指向的就是其上下文的類,而不是 lambda 對象(與匿名類的區別)。在這里 lambda 表達捕獲的變量就是實例變量的接收者 this 而不是實例變量本身。而且可以看到 lambda 的 desugar 方法變成了實例方法,用這種方式,lambda body 幾乎不用做任何轉換只需照搬進方法體就行。還包括對 super 的處理,lambda 對象無法捕獲 super,只能通過調用 this 的實例方法來實現對 super 的調用,可見用 desugar 方法來實現是十分便利的。

this 的捕獲,對于 Android 開發來說特別要注意,在 Activity 中使用 lambda 表達式的話,意味著會通過 final 引用的形象將當前 Activity 實例傳遞到外部去,稍不注意便會引起泄露。一個顯而易見的技巧,將實例字段賦值給局部變量,就不會捕獲 this 引用了。當然對于生命周期相關的對象來說還是不安全的,比如 View。

方法引用

方法引用基本可以當成是 lambda 表達式的一個特例,方法引用都可以用相應的 lambda 表達式來代替,有一個例外就是帶有類型參數方法的函數式接口,能用方法引用但不能用 lambda 表達式,見 java - Lambda Expression and generic method - Stack Overflow。方法引用也分為捕獲與非捕獲,對于無須捕獲接的方法引用主要有:

  • 靜態方法
  • 構造器
  • 未綁定的實例方法

什么是未綁定的實例方法?方法引用語法可以大致認為是接收者::方法名這樣的形式,方法可以是實例方法或者是靜態方法,當方法是實例方法而接收者是類引用時,這時接收者就是一個未綁定的接收者:

list.filter(String::isEmpty)

isEmpty 是實例方法,而接收者是類引用,在這里接收者在運行會被替換為被替換為 list 內的元素,等價于這樣的 lambda 表達式:

list.filter(s -> s.isEmpty())

注意非綁定的實例方法引用是有二義性的,java 根據方法的聲明去推定 isEmpty 是實例方法還是靜態方法,以下面的類為例:

public class C{
   public static boolean isEmpty(C c);
   public boolean isEmpty();
}

如上面的方法聲明兩個方法對于表達式 list.filter(C::isEmpty) 來說都是合法的,java 也就無法推斷出這里是指哪個方法引用,所以編譯器報錯。

需要捕獲的方法引用,也就是已綁定實例的方法引用,包括實例方法,內部類(數組)的構造器,super 方法。接收者就是閉包所要捕獲的變量。但要注意一點方法引用是沒有隱式聲明的 this 引用的。比如下面兩個方法,從語義上來說是等價的,

public void capturingInstance() {
    Predicate<String> c = s -> stri.equals(s);
}

public void capturingIntanceMethod() {
    Predicate<String> c = stri::equals;
}

但是他們捕獲的引用卻不一樣,上文可知 lambda 表達式捕獲的是隱式聲明的 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);
  }
}

還有一點,使用方法引用,因為方法已經是現成的,大部分情況就沒必要重新生成一個 desugar 方法。

但有例外,super 和可變參數,需要一個橋接方法。對于 super 來說,lambda 對象是無法不會當前類的 super 引用的,所以需要借由當前類的實例方法來實現對 super 的引用。

接收者也可以是表達式:

 Predicate<String> c = (stri.equals("abc") ? "abc" : "bcd")::equals;

在這里捕獲的是表達式求值的結果而不是表達式。

所以對于 Activity 來說,要格外注意下面幾種情況可能導致引用泄露

  • this 關鍵字的方法引用
  • super 關鍵字的方法引用
  • 非靜態內部類的構造器引用
  • Activity 或其實例變量可變參數方法引用

可參考 Translation of Lambda Expressions

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內容