Kotlin 內(nèi)聯(lián)函數(shù) inline

Kotlin 中新增了「內(nèi)聯(lián)函數(shù)」,內(nèi)聯(lián)函數(shù)起初是在 C++ 里面的。

那在 Kotlin 中加入內(nèi)聯(lián)函數(shù),是有什么作用呢?

以下內(nèi)容分為以下幾部分:

  1. 什么是 inline 內(nèi)聯(lián)函數(shù)
  2. inline 內(nèi)聯(lián)函數(shù)的作用和使用
    • 2.1 不應(yīng)該使用 inline 的情況
    • 2.2 應(yīng)該使用 inline 的情況
    • 2.3 inline 提高效率的原因
  3. 內(nèi)聯(lián)函數(shù)的一些其他用處;
    • 3.1 支持 return 退出函數(shù)
    • 3.2 禁止內(nèi)聯(lián):noinline
  4. 小結(jié)
  5. 參考鏈接

1. 什么是 inline 內(nèi)聯(lián)函數(shù)呢

簡單來說:
當(dāng)一個函數(shù)被內(nèi)聯(lián) inline 標(biāo)注后,在調(diào)用它的地方,會把這個函數(shù)方法體中的所以代碼移動到調(diào)用的地方,而不是通過方法間壓棧進(jìn)棧的方式。

代碼示例:
1.1 使用 inline 的代碼

// 在 main() 中調(diào)用 makeTest()
fun main() {
    Log.i("zc_test", "main() start")
    makeTest()
    Log.i("zc_test", "main() end")
}
// 內(nèi)聯(lián)函數(shù) makeTest()
private inline fun makeTest() {
    Log.i("zc_test", "makeTest")
}

1.2 使用 inline 編譯成 java 的代碼

public final void main() {
    Log.i("zc_test", "main() start");
    int $i$f$makeTest = false;
    Log.i("zc_test", "makeTest");
    Log.i("zc_test", "main() end");
}

1.3 當(dāng) makeTest() 不在被 inline 修飾時, 被編輯成 java 的代碼為:

public final void main() {
    Log.i("zc_test", "main() start");
    this.makeTest();
    Log.i("zc_test", "main() end");
}

可以看到,當(dāng) makeTest()inline 修飾時, 在 main() 中原來調(diào)用 makeTest() 的地方被替換成了 makeTest() 里面的代碼。

換句話說:在編譯時期,把調(diào)用這個函數(shù)的地方用這個函數(shù)的方法體進(jìn)行替換。

這就是 inline 的本質(zhì)。

至于 Kotlin 內(nèi)聯(lián)函數(shù)有什么作用呢?

2. Kotlin 內(nèi)聯(lián)函數(shù)的作用和使用
由上面可以知道, inline 的本質(zhì):在編譯時期,把調(diào)用這個函數(shù)的地方用這個函數(shù)的方法體進(jìn)行替換。

那么我們什么時候應(yīng)該使用 inline 什么時候不應(yīng)該使用呢?

2.1 不應(yīng)該使用 inline 的情況
當(dāng)使用 inline 標(biāo)注時,如果是下面這樣,無參數(shù)的函數(shù)時:

//makeTest() 沒有任何的參數(shù)
private inline fun makeTest() {
     Log.i("zc_test", "makeTest")
}
//或者帶有基本變量參數(shù)的函數(shù),編譯器也會報錯。
private inline fun makeTest2(test: String) {
     Log.i("zc_test", "makeTest")
}

這個時候 AndroidStudio 編譯器會在 inline 位置有黃色警告,
Expected performance impact of inlining '...' can be insignificant. Inlining works best for functions with lambda parameters,
翻譯過來就是,在這個位置使用 inline 并不會有很大的提高,inline 適合在包含 lambda 參數(shù)的函數(shù)上。

也就是說 inline 在一般的方法是標(biāo)注,是不會起到很大作用的,inline 能帶來的性能提升,往往是在參數(shù)是 lambda 的函數(shù)上。


在一篇文章上看到這樣一段話:
眾所周知,JVM 內(nèi)部已經(jīng)實現(xiàn)了內(nèi)聯(lián)優(yōu)化,它會在任何可以通過內(nèi)聯(lián)來提升性能的地方將函數(shù)調(diào)用內(nèi)聯(lián)化,并且相對于手動將普通函數(shù)定義為內(nèi)聯(lián),通過 JVM 內(nèi)聯(lián)優(yōu)化所生成的字節(jié)碼,每個函數(shù)的實現(xiàn)只會出現(xiàn)一次,這樣在保證減少運行時開銷的同時,也沒有增加字節(jié)碼的尺寸;
鏈接:http://m.itdecent.cn/p/678a49054238
來源:簡書

這段話也在間接證明了編譯器給的警告,inline 不適合在無參數(shù)的函數(shù)中, 適合在包含 lambda 參數(shù)的函數(shù)上。


2.2 應(yīng)該使用 inline 的地方: 帶有 lambda 參數(shù)的函數(shù)

當(dāng)我們寫一個會被經(jīng)常調(diào)用的帶 lambda參數(shù)的函數(shù)時, 可使用該方式。

例如代碼:

// body 是本身一個函數(shù)
fun foo(body:() -> Unit) {
    println("foo() hahaha")
    ordinaryFunction(body)
}

inline fun ordinaryFunction(block: () -> Unit) {
    println("hahha")
    block.invoke()
    println("hahha233333")
}

在上述代碼中,我們把 foo() 的函數(shù)參數(shù) body 作為一個參數(shù)傳遞給 ordinaryFunction() ,

這是我們可以通過在 ordinaryFunction() 上面標(biāo)注 inline 從而使得方法的調(diào)用棧少一層,使得代碼變?yōu)椋?/p>

fun foo(body:() -> Unit) {
    println("hahha")
    block.invoke()
    println("hahha233333")
}

2.3 inline 的使用規(guī)則
那么什么時候使用,什么時候不使用 inline 呢?
根據(jù)上面,我們大致可分為兩種:

  1. 不帶參數(shù),或是帶有普通參數(shù)的函數(shù),不建議使用 inline
  2. 帶有 lambda 函數(shù)參數(shù)的函數(shù),建議使用 inline

2.3 inline 提高效率的原因

為什么要使用 inline 呢?必然是因為使用 inline 會帶來效率的提升。
我們比較一下使用了 inline 和不使用 inline 編譯成 java 代碼的差異

當(dāng)然上述 ordinaryFunction() 也可以不使用 inline 標(biāo)注,我們看一下編譯成 java 的代碼樣式, 「對比」添加了 inline 的標(biāo)注的 java 代碼,我們發(fā)現(xiàn),當(dāng)不添加 inline 時,代碼中,多出了一個類:

final class TestInline$main$1$1 extends Lambda implements Function0 {
   public static final TestInline$main$1$1 INSTANCE = new TestInline$main$1$1();
   // $FF: synthetic method
   // $FF: bridge method
   public Object invoke() {
      this.invoke();
      return Unit.INSTANCE;
   }

   public final void invoke() {
   }

   TestInline$main$1$1() {
      super(0);
   }
}

它便是在編譯過程中,因為 lambda 參數(shù) 多出來的類,無疑中會增加內(nèi)存的分配。

所以我們就知道了,在 kotlin 中,因為出現(xiàn)了大量的 高階函數(shù) -- 「高階函數(shù)是將函數(shù)用作參數(shù)或返回值的函數(shù)」,使得越來越多的地方出現(xiàn) 函數(shù)參數(shù) 不斷傳遞的現(xiàn)象,每一個函數(shù)參數(shù)都會被編譯成一個對象, 使得內(nèi)存分配(對于函數(shù)對象和類)和虛擬調(diào)用會增加運行時間開銷。所以才會出現(xiàn) inline 內(nèi)聯(lián)函數(shù)??梢酝ㄟ^ inline 的標(biāo)注,把原本需要生成一個類的開銷節(jié)省了, 同時也少了一層方法棧的調(diào)用。

3. inline 的其他作用

除了上述的功能點外,還有一些值得注意的小地方。

3.1 支持 return 退出函數(shù)
在編碼中,我們通常習(xí)慣使用 return 返回退出這個函數(shù),但是 lambda 表達(dá)式不能使包含它的函數(shù)返回。

例如代碼:

fun foo(body:()->Unit) {
    ordinaryFunction {
        println("zc_testlabama 表達(dá)式退出")
        return
    }
    println("zc_test --->foo() end")
} 
fun ordinaryFunction(block: () -> Unit) {
    println("hahha")
    block.invoke()
    println("hahha233333")
}

如果在 ordinaryFunction 這個方法沒有 inline 的標(biāo)注,編譯器會在 return 的位置出錯,return is not allowed here.

解決上述錯誤的方式,可以為 return 添加標(biāo)簽,例如 return@ordinaryFunction, 但是這樣的話,方法執(zhí)行只會退出 lambda 表達(dá)式,后面的代碼 println("zc_test --->foo() end") 還是會走到的。

當(dāng)我們添加上 inline 時,正確的代碼如下:

fun foo(body:()->Unit) {
    ordinaryFunction {
        // 因為標(biāo)識為 inline 的函數(shù)會被插入到調(diào)用出,此時 return 肯定是 return 到該整個方法
        println("zc_testlabama 表達(dá)式退出")
        return
    }
    println("zc_test --->foo() end")
}
// 如果不使用 inline, 上面代碼會被報錯。因為「不允許這么做」
inline fun ordinaryFunction(block: () -> Unit) {
    println("hahha")
    block.invoke()
    println("hahha233333")
}

當(dāng)我們添加了 inline 標(biāo)志后,在 ordinaryFunction{}return 時就會退出整個 foo() 函數(shù),因此結(jié)尾的 println("zc_test --->foo() end") 是不會被調(diào)用的。

inline 可以讓函數(shù)參數(shù)里面的 return 生效

kotlin 官方注釋:breakcontinue 在內(nèi)聯(lián)的 lambda 表達(dá)式中還不可用,但我們也計劃支持它們。

3.2 禁止內(nèi)聯(lián):noinline

為什么會有 noinline呢?為什么需要這種方式呢?

官網(wǎng)中這么寫著:如果希望只內(nèi)聯(lián)一部分傳給內(nèi)聯(lián)函數(shù)的 lambda 表達(dá)式參數(shù),那么可以用 noinline 修飾符標(biāo)記不希望內(nèi)聯(lián)的函數(shù)參數(shù), 代碼如:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { …… }

什么時候我們會需要 noinline 呢?
例如代碼:

inline fun foo(testName:String, body:()->Unit) {
    // 這里會報錯。。。
    ordinaryFunction(body)
    println("zc_test --->foo() end")
} 
fun ordinaryFunction(block: () -> Unit) {
    println("hahha")
    block.invoke()
    println("hahha233333")
}

如果 ordinaryFunction() 不使用 inline 標(biāo)注,是一般的函數(shù),這里是不允許把內(nèi)聯(lián)函數(shù) foo() 的函數(shù)參數(shù) body 傳遞給 ordinaryFunction()。

即:內(nèi)聯(lián)函數(shù)的「函數(shù)參數(shù)」 不允許作為參數(shù)傳遞給非內(nèi)聯(lián)的函數(shù),
如果我們想要實現(xiàn)上述的調(diào)用,便可以使用 noinline 標(biāo)注內(nèi)聯(lián)函數(shù) foo()body 參數(shù)

inline fun foo(testName:String, noinline body:()->Unit) {
    ...
}

上述代碼便可以正常運行了。

4. 小結(jié):

上面對 inline 做了一些簡單的介紹。大部分都是實踐中產(chǎn)生的結(jié)論。
使用了很多代碼,這是不可避免的,只有使用多了,才會比較熟悉這些 kotlin 中的屬性。

當(dāng)然也很局限,個人水平有限,有如錯誤,還請指出。

5. 參考鏈接:

簡書:http://m.itdecent.cn/p/678a49054238

官網(wǎng):https://www.kotlincn.net/docs/reference/inline-functions.html

文章來自: kotlin inline 初步解析

2019.10.24 by chendroid

PS: 1024 程序員日,愿每個認(rèn)真開發(fā)的工程師,都能夠生活開心,工作順利,身體健康,錢多,活少,身體好!

相關(guān)文章:kotlin 作用域函數(shù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容