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 編譯的庫。
原圖見 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 或其實例變量可變參數方法引用