在Kotlin中,使用高階函數(shù)(函數(shù)/Lambda作為參數(shù)傳遞)時不良使用會造成性能問題乔煞。官方文檔表述如下:
kotlin中每一個函數(shù)都是一個對象褂傀,并且會捕獲一個閉包。 即那些在函數(shù)體內(nèi)會訪問到的變量编检。
內(nèi)存分配(對于函數(shù)對象和類)和虛擬調(diào)用會引入運行時間開銷胎食。
那在什么情況下函數(shù)會捕獲閉包,性能隱患是怎么產(chǎn)生的允懂,又是什么時候需要使用內(nèi)聯(lián)inline呢厕怜?
下面通過幾個場景來分析
1.Lambda不訪問外部
val k = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
testInline {
Logger.d("qintong", "funn33 $it")
}
}
private fun testInline(func : (i : Int) -> Unit) {
func(k)
}
用AndroidStudio自帶工具查看對應的字節(jié)碼,為方便查看直接再講該字節(jié)碼反編譯成java代碼累驮,對應的onCreate()部分:
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.testInline((Function1)null.INSTANCE);
}
這里為什么是(Function1)null.INSTANCE酣倾?stackoverflow一下:
https://stackoverflow.com/questions/53384931/why-kotlin-decompiler-generates-null-instance
kotlin代碼轉(zhuǎn)成字節(jié)碼,字節(jié)碼再轉(zhuǎn)成java代碼可能會出現(xiàn)錯誤谤专,那我們直接分析字節(jié)碼躁锡。
對應onCreate()中testInline()方法調(diào)用時節(jié)碼如下:
GETSTATIC com/xxx/xxx/xxx/xxx/x/xxxx/XxxActivity$onCreate$1.INSTANCE : Lcom/xxx/xxx/xxx/xxx/xxx/xxx/XxxActivity$onCreate$1;
CHECKCAST kotlin/jvm/functions/Function1
INVOKESPECIAL com/xxx/xxx/xxx/xxx/x/xxxx/XxxActivity.testInline (Lkotlin/jvm/functions/Function1;)V
L3
可見在編譯成字節(jié)碼顯示:
- GETSTATIC指令取出的靜態(tài)變量值onCreate$1.INSTANCE,然后推入操作數(shù)棧頂
- CHECKCAST 檢查類型
- INVOKESPECIAL 調(diào)用testInline()置侍,彈出棧頂參數(shù)onCreate$1.INSTANCE映之,傳入testInline()
由此可見編譯成字節(jié)碼后對應傳入testInline()方法的Lambda以Lkotlin/jvm/functions/Function1類型的靜態(tài)對象onCreate$1.INSTANCE存在。假如onCreate()被反復調(diào)用蜡坊,由于Lambda對應的方法對象為靜態(tài)對象杠输,應該不存在明顯的性能問題。
接下來測試testInline()加上inline后:
用同樣方法秕衙,kotlin > 字節(jié)碼 > java代碼:
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
int $i$f$testInline = false;
int it = this.getK();
int var5 = false;
Logger.d("qintong", "funn33 " + it);
}
由此可見testInline()已平鋪到onCreate()中蠢甲,不生成內(nèi)部對象了。
2.Lambda訪問外部類的成員方法
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val x = 1
testInline{
log(it)
}
}
fun log(it: Int) {
Logger.d("qintong", "funn22 $it")
}
private fun testInline(func : (i : Int) -> Unit) {
func(k)
}
同樣方法得到對應java代碼:
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
int x = true;
this.testInline((Function1)(new Function1() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
this.invoke(((Number)var1).intValue());
return Unit.INSTANCE;
}
public final void invoke(int it) {
VersionActivity.this.log(it);
}
}));
}
可見Lambda以匿名內(nèi)部類的形式傳入testInline()方法中据忘。每次testInline()調(diào)用都會new一個Function1對象鹦牛。此時由于Lambda引用了外部對象的方法,導致其編譯后難以像第一個例子中優(yōu)化成一個靜態(tài)內(nèi)部對象勇吊。
加入onCreate()方法被循環(huán)調(diào)用曼追,每次調(diào)用都會new出一個Function1對象,這會造成一些性能問題:不斷創(chuàng)建對象會造成內(nèi)存抖動汉规,增加gc負擔礼殊,頻繁gc也會造成卡頓。所以此時需要將函數(shù)內(nèi)聯(lián):
testInline()加上inline后:
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
int x = true;
Function1 funxx = (Function1)null.INSTANCE;
int $i$f$testInline = false;
int it = this.getK();
int var7 = false;
int $i$f$log = false;
Logger.d("qintong", "funn22 " + it);
}
和第一個例子一樣,內(nèi)聯(lián)后testInlint()方法平鋪到了onCreate()中晶伦,不存在性能問題了碟狞。
3.Lambda內(nèi)訪問外部的變量
和上面的例子一樣,我們對下面進行測試:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val x = 1
testInline{
Logger.d("qintong", "funn11 $it + $x")
}
}
private fun testInline(func : (i : Int) -> Unit) {
func(k)
}
對應java代碼:
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final int x = 1;
this.testInline((Function1)(new Function1() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
this.invoke(((Number)var1).intValue());
return Unit.INSTANCE;
}
public final void invoke(int it) {
Logger.d("qintong", "funn11 " + it + " + " + x);
}
}));
}
和第二個例子一樣坝辫,也是會在每次調(diào)用時new出對象傳入testInline()中篷就,同樣有性能問題。
加inline后近忙,結(jié)果和前面兩個例子一樣竭业,就不贅述了。
結(jié)論
- 從實質(zhì)上及舍,kotlin中使用高階函數(shù)時每一個函數(shù)都對應個對象傳遞給調(diào)用方未辆。
- 使用高階函數(shù)時當函數(shù)/Lambda不訪問外部的變量/方法(即不捕獲外部)時,編譯器會將函數(shù)對應的對象優(yōu)化成類的靜態(tài)成員變量锯玛,反復調(diào)用時不會有性能問題咐柜。此時也不需要使用inline。
- 當函數(shù)/Lambda捕獲外部時攘残,比如訪問閉包內(nèi)的參數(shù)拙友、訪問外部方法時,閉包會一new 內(nèi)部類對象的方式進行傳遞歼郭,此時如果方法被頻繁調(diào)用(如在循環(huán)中被調(diào)用)會造成性能問題:對象被持續(xù)創(chuàng)建遗契,造成內(nèi)存抖動,增加gc負擔病曾,頻繁gc也可能造成卡頓牍蜂。
- 使用inline,方法會被平鋪到調(diào)用處泰涂,不存在上面說的性能問題鲫竞。
- inline的使用不當也會有負面作用:由于inline是將函數(shù)平鋪到調(diào)用處,所以要避免內(nèi)聯(lián)函數(shù)過大逼蒙。