在Kotlin中,使用高階函數(shù)(函數(shù)/Lambda作為參數(shù)傳遞)時(shí)不良使用會(huì)造成性能問(wèn)題。官方文檔表述如下:
kotlin中每一個(gè)函數(shù)都是一個(gè)對(duì)象,并且會(huì)捕獲一個(gè)閉包。 即那些在函數(shù)體內(nèi)會(huì)訪問(wèn)到的變量。
內(nèi)存分配(對(duì)于函數(shù)對(duì)象和類)和虛擬調(diào)用會(huì)引入運(yùn)行時(shí)間開(kāi)銷。
那在什么情況下函數(shù)會(huì)捕獲閉包,性能隱患是怎么產(chǎn)生的,又是什么時(shí)候需要使用內(nèi)聯(lián)inline呢?
下面通過(guò)幾個(gè)場(chǎng)景來(lái)分析
1.Lambda不訪問(wèn)外部
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自帶工具查看對(duì)應(yīng)的字節(jié)碼,為方便查看直接再講該字節(jié)碼反編譯成java代碼,對(duì)應(yīng)的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代碼可能會(huì)出現(xiàn)錯(cuò)誤,那我們直接分析字節(jié)碼。
對(duì)應(yīng)onCreate()中testInline()方法調(diào)用時(shí)節(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àn)在編譯成字節(jié)碼顯示:
- GETSTATIC指令取出的靜態(tài)變量值onCreate$1.INSTANCE,然后推入操作數(shù)棧頂
- CHECKCAST 檢查類型
- INVOKESPECIAL 調(diào)用testInline(),彈出棧頂參數(shù)onCreate$1.INSTANCE,傳入testInline()
由此可見(jiàn)編譯成字節(jié)碼后對(duì)應(yīng)傳入testInline()方法的Lambda以Lkotlin/jvm/functions/Function1類型的靜態(tài)對(duì)象onCreate$1.INSTANCE存在。假如onCreate()被反復(fù)調(diào)用,由于Lambda對(duì)應(yīng)的方法對(duì)象為靜態(tài)對(duì)象,應(yīng)該不存在明顯的性能問(wèn)題。
接下來(lái)測(cè)試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);
}
由此可見(jiàn)testInline()已平鋪到onCreate()中,不生成內(nèi)部對(duì)象了。
2.Lambda訪問(wèn)外部類的成員方法
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)
}
同樣方法得到對(duì)應(yīng)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);
}
}));
}
可見(jiàn)Lambda以匿名內(nèi)部類的形式傳入testInline()方法中。每次testInline()調(diào)用都會(huì)new一個(gè)Function1對(duì)象。此時(shí)由于Lambda引用了外部對(duì)象的方法,導(dǎo)致其編譯后難以像第一個(gè)例子中優(yōu)化成一個(gè)靜態(tài)內(nèi)部對(duì)象。
加入onCreate()方法被循環(huán)調(diào)用,每次調(diào)用都會(huì)new出一個(gè)Function1對(duì)象,這會(huì)造成一些性能問(wèn)題:不斷創(chuàng)建對(duì)象會(huì)造成內(nèi)存抖動(dòng),增加gc負(fù)擔(dān),頻繁gc也會(huì)造成卡頓。所以此時(shí)需要將函數(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);
}
和第一個(gè)例子一樣,內(nèi)聯(lián)后testInlint()方法平鋪到了onCreate()中,不存在性能問(wèn)題了。
3.Lambda內(nèi)訪問(wèn)外部的變量
和上面的例子一樣,我們對(duì)下面進(jìn)行測(cè)試:
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)
}
對(duì)應(yīng)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);
}
}));
}
和第二個(gè)例子一樣,也是會(huì)在每次調(diào)用時(shí)new出對(duì)象傳入testInline()中,同樣有性能問(wèn)題。
加inline后,結(jié)果和前面兩個(gè)例子一樣,就不贅述了。
結(jié)論
- 從實(shí)質(zhì)上,kotlin中使用高階函數(shù)時(shí)每一個(gè)函數(shù)都對(duì)應(yīng)個(gè)對(duì)象傳遞給調(diào)用方。
- 使用高階函數(shù)時(shí)當(dāng)函數(shù)/Lambda不訪問(wèn)外部的變量/方法(即不捕獲外部)時(shí),編譯器會(huì)將函數(shù)對(duì)應(yīng)的對(duì)象優(yōu)化成類的靜態(tài)成員變量,反復(fù)調(diào)用時(shí)不會(huì)有性能問(wèn)題。此時(shí)也不需要使用inline。
- 當(dāng)函數(shù)/Lambda捕獲外部時(shí),比如訪問(wèn)閉包內(nèi)的參數(shù)、訪問(wèn)外部方法時(shí),閉包會(huì)一new 內(nèi)部類對(duì)象的方式進(jìn)行傳遞,此時(shí)如果方法被頻繁調(diào)用(如在循環(huán)中被調(diào)用)會(huì)造成性能問(wèn)題:對(duì)象被持續(xù)創(chuàng)建,造成內(nèi)存抖動(dòng),增加gc負(fù)擔(dān),頻繁gc也可能造成卡頓。
- 使用inline,方法會(huì)被平鋪到調(diào)用處,不存在上面說(shuō)的性能問(wèn)題。
- inline的使用不當(dāng)也會(huì)有負(fù)面作用:由于inline是將函數(shù)平鋪到調(diào)用處,所以要避免內(nèi)聯(lián)函數(shù)過(guò)大。