Lambda - 認識java lambda與kotlin lambda的細微差異

語言: CN / TW / HK

theme: orange

Lambda

這個估計算是一個非常有歷史感的話題了,Lambda相關的文章,也有很多了,為啥還要拿出來炒炒冷飯呢?主要是最近有對Lambda的內容進行位元組碼處理,同時Lambda在java/kotlin/android中,都有著不一樣是實現,非常有趣,因此本文算是一個記錄,讓我們一起去走進lambda的世界吧。當然,本文以java/kotlin視角去記錄,在android中lambda的處理還不一樣,我們先挖個坑,看看有沒有機會填上,當然,部分的我也會夾雜的一起說!

最簡單的例子

比如我們常常在寫ui的時候,設定一個監聽器,就是這麼處理 view.setOnClickListener(v -> { Log.e("hello","123"); });

編譯後的位元組碼

INVOKEDYNAMIC onClick()Landroid/view/View$OnClickListener; [ // handle kind 0x6 : INVOKESTATIC java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; // arguments: (Landroid/view/View;)V, // handle kind 0x6 : INVOKESTATIC 這裡就是我們要的方法 類名.lambda$myFunc$0(Landroid/view/View;)V, (Landroid/view/View;)V emmm,密密麻麻,我們先不管這個,這裡主要是INVOKEDYNAMIC的這個指令,這裡我就不再重複INVOKEDYNAMIC的由來之類的了,我們直接來看,INVOKEDYNAMIC指令執行後的產物是啥?

生成產物類

首先產物之一,肯定是setOnClickListener裡面需要的一個實現OnClickListener的物件對吧!我們都知道INVOKEVIRTUAL會在運算元棧的執行一個消耗“物件”的操作,這個從哪裡來,其實也很明顯,就是從INVOKEDYNAMIC執行後被放入運算元棧的。

mermaid graph TD INVOKEDYNAMIC --> 生出來了OnClickListener -->INVOKEVIRTUAL消耗

當然,這個生成的類還是比較難找的,可以通過以下明=命令去翻翻

java -Djdk.internal.lambda.dumpProxyClasses 類路徑

當然,在AS中也有相關的生成類,在intermediates/transform目錄下,不過高版本的我找不到在哪了,如果知道的朋友也可以告訴一下

呼叫特定方法

我們的產物類有了,但是我們也知道,lambda不是生成一個物件那麼簡單,而是要呼叫到裡面的閉包方法,比如我們本例子就是

v -> { Log.e("hello","123"); } 那麼我們這個產物的方法在哪呢? 回到INVOKEDYNAMIC指令的裡面,我們看到

java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; // arguments: (Landroid/view/View;)V, // handle kind 0x6 : INVOKESTATIC 類名.lambda$myFunc$0(Landroid/view/View;)V, (Landroid/view/View;)V ] INVOKEVIRTUAL android/view/View.setOnClickListener (Landroid/view/View$OnClickListener;)V

這裡有很多新的東西,比如LambdaMetafactory(java建立執行時類),MethodHandles等,相關概念我不贅述啦!因為有比我寫的更好的文章,大家可以觀看一下噢! ASM對匿名內部類、Lambda及方法引用的Hook研究

我這裡特地拿出來

INVOKESTATIC 類名.lambda$myFunc$0(Landroid/view/View;)V 這裡會在生成的產物類中,直接通過INVOKESTATIC方式(當然,這裡只針對我們這個例子,後面會繼續有說明,不一定是通過INVOKESTATIC方式)方法是lambda$myFunc$0,我們找下這個方法,可以看到,還真的有,如下

``` private static synthetic lambda$myFunc$0(Landroid/view/View;)V L0 LINENUMBER 14 L0 LDC "hello" LDC "123" INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I POP

} ``` 這個方法就是lambda要執行的方法,只不過在位元組碼中包裝了一層。

至此,我們就能夠大概明白了,lambda究竟幹了些什麼

java lambda vs Koltin lambda

java lambda

我們剛剛有提到,生成的產物方法不一定通過INVOKESTATIC的方式呼叫,這也間接說明了,我們的lambda的包裝方法,不一定是static,即不一定是靜態的。

我們再來一文,

Lambda 設計參考

簡單來說,java lambda按照情況,生成的方法也不同,比如當前我們的例子,它其實是一個無狀態的lambda,即當前塊作用域內,就能捕獲到所需要的引數,所以就能直接生成一個static的方法

這裡我們特地說明了塊作用域,比如,下面的方法,setOnClickListener裡面的lambda也依賴了一個變數a,但是他們都屬於同一個塊級別(函式內), void myFunc(View view){ int a = 1; view.setOnClickListener(v -> { Log.e("hello","123" +a ); }); }

生成依舊是一個static方法

``` private static synthetic lambda$myFunc$0(ILandroid/view/View;)V L0 LINENUMBER 15 L0 LDC "hello" NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder. ()V LDC "123" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ILOAD 0 INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I POP

} ```

但是,如果我們依賴當前類的一個變數,比如 ``` 類屬性 public String s;

void myFunc(View view){

view.setOnClickListener(v -> {
    Log.e("hello","123" +s);
});

} ``` 此時就生成一個當前類的例項方法,在當前類可以呼叫到該方法

private synthetic lambda$myFunc$0(Landroid/view/View;)V L0 LINENUMBER 15 L0 LDC "hello" NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V LDC "123" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ALOAD 0 GETFIELD com/example/suanfa/TestCals.s : Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I POP } 同時我們也看到,這種方式會引入ALOAD 0,即this指標被捕獲,因此,假如外層類與lambda生命週期不同步,就會導致記憶體洩漏的問題,這點需要注意噢!!同時我們也要注意,並不是所有lambda都會,像上面我們介紹的lambda就不會!

kotlin lambda

這裡特地拿kotlin 出來,是因為它有與java層不一樣的點,比如同樣的程式碼,lambda依賴了外部類的屬性,生成的方法還是一個靜態的方法,而不是例項方法 var s: String = 123 fun test(view:View){ view.setOnClickListener { Log.e("hello","$s") } } 位元組碼如下

不一樣的點,選擇多一個外部類的引數 private final static test$lambda-0(Lcom/example/suanfa/TestKotlin;Landroid/view/View;)V L0 ALOAD 0 LDC "this$0" INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V L1 LINENUMBER 11 L1 LDC "hello" ALOAD 0 GETFIELD com/example/suanfa/TestKotlin.s : Ljava/lang/String; INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String; INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I POP L2 LINENUMBER 12 L2 RETURN L3 LOCALVARIABLE this$0 Lcom/example/suanfa/TestKotlin; L0 L3 0 LOCALVARIABLE it Landroid/view/View; L0 L3 1 MAXSTACK = 2 MAXLOCALS = 2 }

同樣的,同一塊作用域的,也當然是靜態方法 fun test(view:View){ val s = "123" view.setOnClickListener { Log.e("hello","$s") } } 如下,比起依賴了外部類的屬性,沒有依賴的話,自然也不用把外部類物件當作引數傳入 private final static test$lambda-0(Ljava/lang/String;Landroid/view/View;)V L0 ALOAD 0 LDC "$s" INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V L1 LINENUMBER 11 L1 LDC "hello" ALOAD 0 INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String; INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I POP L2 LINENUMBER 12 L2 RETURN L3 LOCALVARIABLE $s Ljava/lang/String; L0 L3 0 LOCALVARIABLE it Landroid/view/View; L0 L3 1 MAXSTACK = 2 MAXLOCALS = 2 }

因此,我們可以通過這兩個差異,可以做一些特定的位元組碼邏輯。

總結

lambda的水還是挺深的,我們可以通過本文,去初步瞭解一些lambda的知識,同時我們也需要注意,在android中,也為了相容lambda,做了一定的騷操作,比如我們常說的d8會對desuger做了一些操作等等。同時android的生成產物類,也會做單例的優化,這在一些場景會有不一樣的坑,我們之後再見啦!