Lambda - 認識java lambda與kotlin lambda的細微差異
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,即不一定是靜態的。
我們再來一文,
簡單來説,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.
} ```
但是,如果我們依賴當前類的一個變量,比如 ``` 類屬性 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的生成產物類,也會做單例的優化,這在一些場景會有不一樣的坑,我們之後再見啦!
- Lambda - 認識java lambda與kotlin lambda的細微差異
- 寫給android開發的Linux 信號 - 下篇
- 神奇的年終總結
- “Signal”背後的bug與解決
- Parcel-Binder流水線的打包俠
- 避免踩坑,內存不足時系統回收Activity的流程解析
- Suspend函數與回調的互相轉換
- 聊一聊Kotlin協程"低級"api
- Android性能優化 - 包體積殺手之R文件內聯原理與實現
- 從val跟var瞭解虛擬機世界
- Android性能優化 - 捕獲java crash的那些事
- Android性能優化-線程監控與線程統一
- Android性能優化 - plt hook 與native線程監控
- 神奇的Compose - 性能監控篇
- 最近很火的反調試,你知道它是什麼嗎?
- 神奇的共享內存
- 學完ASM Tree api,再也不怕hook了
- 對移動端app容災的思考
- 為什麼説獲取堆棧從來就不是一件簡單的事情
- 聽説Compose與RecyclerView結合會有水土不服?