1. 簡(jiǎn)介
inline、noinline、crossinline 是 Kotlin 中的三個(gè)關(guān)鍵字。在 Kotlin 源碼中我們可以發(fā)現(xiàn),這三個(gè)關(guān)鍵字被大量使用,那么它們究竟是干嘛的呢?
推薦:后續(xù)文章將在GZH「碼途有道」發(fā)布,其他博客平臺(tái)不定時(shí)更新!
2. inline
2.1 編譯時(shí)常量
在介紹 inline 之前,我們可以先了解下 Java 中的一個(gè)概念:編譯時(shí)常量 ( Compile-time Constant )。其指變量的值固定不變,在編譯時(shí)我們就可拿到這個(gè)變量的值。具體到代碼中,就是變量使用 final 修飾,類型只能是字符串或基本類型,在聲明時(shí)就賦值;在編譯時(shí),會(huì)直接拿這個(gè)變量值去替換變量名。這樣一來(lái),程序結(jié)構(gòu)就變簡(jiǎn)單了。
2.2 inline: 內(nèi)聯(lián)函數(shù)
上述編譯時(shí)常量的作用其實(shí)就是一種內(nèi)聯(lián)優(yōu)化,而被 inline 標(biāo)記的函數(shù),被稱為內(nèi)聯(lián)函數(shù)。所謂內(nèi)聯(lián),就是在代碼編譯時(shí)會(huì)將被調(diào)用處代碼直接嵌入到調(diào)用處的函數(shù)體中。具體示例如下:
// 內(nèi)聯(lián)函數(shù)
inline fun hello() {
println("Say Hello!")
}
// 調(diào)用函數(shù)
fun main() {
hello()
}
// 實(shí)際編譯后
fun main() {
println("Say Hello!")
}
可以看出,inline 的作用與編譯時(shí)常量的特點(diǎn)非常相似,都可以內(nèi)嵌代碼,我們也可以將此種行為稱作代碼的展開鋪平。如此一來(lái),內(nèi)聯(lián)函數(shù)就可以減少方法棧的層級(jí),省去了上述示例中 hello() 在方法棧中的占用層級(jí),使得方法棧變淺。
但事實(shí)上,減少方法棧的層級(jí),這種優(yōu)化產(chǎn)生的效果非常小,小到可以忽略不計(jì)。而甚至可能因?yàn)榇罅渴褂?inline,代碼到處拷貝,最后使得編譯生成的字節(jié)碼膨脹,成為負(fù)優(yōu)化。那么,inline 到底有何優(yōu)勢(shì)呢?
2.3 inline: 鋪平函數(shù)類型參數(shù)
我們?cè)谄匠i_發(fā)中會(huì)經(jīng)常遇到函數(shù)中的參數(shù)也是函數(shù)類型,如下:
fun hello(block: () -> Unit) {
println("Say Hello!")
block()
}
fun main() {
hello {
println("Bye!")
}
}
在 Java 中并沒(méi)有對(duì)函數(shù)類型變量的原生支持,此形式是 Kotlin 中的寫法。而 Kotlin 為了讓其生效,它會(huì)創(chuàng)建一個(gè) JVM 對(duì)象作為函數(shù)類型變量的實(shí)際載體去執(zhí)行變量中的實(shí)際代碼。也就是說(shuō),在 main() 中調(diào)用 hello() 時(shí),其實(shí)創(chuàng)建了一個(gè)對(duì)象來(lái)執(zhí)行 hello() 的 Lambda 表達(dá)式中的代碼,雖然這個(gè)對(duì)象只是用一下就會(huì)被拋棄,但是確實(shí)創(chuàng)建了。這就實(shí)實(shí)在在的產(chǎn)生了資源消耗。而如果 hello() 被循環(huán)調(diào)用100次,甚至更多,那么內(nèi)存占用不就一下子上來(lái)了嗎!那如果給 hello() 加上 inline 會(huì)有什么效果呢?
inline fun hello(block: () -> Unit) {
println("Say Hello!")
block()
}
fun main() {
hello {
println("Bye!")
}
}
// 實(shí)際編譯后
fun main() {
println("Say Hell!")
println("Bye!")
}
可以看到,如果將 hello() 變?yōu)閮?nèi)聯(lián)函數(shù),編譯后不僅將 hello() 自身代碼鋪平,同時(shí)也將 hello() 的 Lambda 表達(dá)式中的代碼也一塊在 main() 中鋪平,沒(méi)有額外創(chuàng)建對(duì)象,這樣資源消耗也就自然減少了,這就是 inline 非常重要的一個(gè)特性:可以將被標(biāo)記函數(shù)的函數(shù)類型參數(shù)代碼一起展開鋪平。
2.4 正確使用inline
綜上可知,inline 可以優(yōu)化代碼,但是不能無(wú)腦使用,否則會(huì)造成字節(jié)碼膨脹,最直觀的體現(xiàn)就是包體積變大。一般我們可以在函數(shù)高頻調(diào)用場(chǎng)景酌情使用,減少內(nèi)存占用,優(yōu)化代碼執(zhí)行速度(畢竟鋪平的代碼要比調(diào)用執(zhí)行的更快),例如處理Socket推送數(shù)據(jù)的場(chǎng)景。
3. noinline
根據(jù)上述內(nèi)容,我們可知:函數(shù)類型參數(shù)本質(zhì)是一個(gè)對(duì)象,而 inline 函數(shù)會(huì)將這個(gè)對(duì)象的代碼展開鋪平,消除其對(duì)象屬性。
noinline 字面意思是“不內(nèi)聯(lián)”,用于標(biāo)記 inline 函數(shù)中的函數(shù)類型參數(shù)。被標(biāo)記的函數(shù)類型參數(shù)不會(huì)被內(nèi)聯(lián),即不會(huì)如上示例所述,進(jìn)行代碼鋪平,其依舊是個(gè)對(duì)象。那么關(guān)閉內(nèi)聯(lián)優(yōu)化,有什么用呢?我們可以看下面一個(gè)例子:
// 錯(cuò)誤示例
inline fun hello(block: () -> Unit): () -> Unit {
println("Say Hello!")
return block
}
我們有時(shí)會(huì)碰到返回值就是函數(shù)類型參數(shù)這種狀況,但是 inline 函數(shù)已經(jīng)將 hello() 中的 block 代碼鋪平,即本來(lái) block 是個(gè)對(duì)象,現(xiàn)在被 inline 優(yōu)化,直接消除了,那還怎么返回呢?所以上述示例是錯(cuò)誤的,同時(shí)我們也能在 IDE 中發(fā)現(xiàn) return 直接報(bào)錯(cuò)了。這時(shí)就輪到 noinline 出場(chǎng),關(guān)閉針對(duì)函數(shù)類型參數(shù)的內(nèi)聯(lián)優(yōu)化,使 block 依然作為一個(gè)對(duì)象被使用,如此就可以正常 return 了。正確示例如下:
// 正確示例,使用 noinline 標(biāo)記 block
inline fun hello(noinline block: () -> Unit): () -> Unit {
println("Say Hello!")
return block
}
4. crossinline
4.1 Lambda中的return
在說(shuō) crossinline 之前,我們先看兩個(gè)示例:
// 示例 1
inline fun hello(block: () -> Unit) {
println("Say Hello!")
block()
}
fun main() {
hello {
println("Bye!")
return
}
println("Continue")
}
// 示例 2
fun hello(block: () -> Unit) {
println("Say Hello!")
block()
}
fun main() {
hello {
println("Bye!")
return@hello
}
println("Continue")
}
在示例1中,我們?cè)?inline 標(biāo)記的 hello() 的 Lambda 表達(dá)式中加了 return;而在示例2中我們也在 hello() 的 Lambda 表達(dá)式中加了 return,但是與示例1不同的是,示例2中的 hello() 并不是內(nèi)聯(lián)函數(shù),且 return 后面多了 @hello,即顯示的結(jié)束 hello(),不允許直接使用 return。我們看下兩個(gè)示例的打印結(jié)果:
示例1的打印結(jié)果:
Say Hello!
Bye!
示例2的打印結(jié)果:
Say Hello!
Bye!
Continue
可以發(fā)現(xiàn),示例1中 Continue 并沒(méi)有被打印出來(lái),所以 hello() 的 Lambda 表達(dá)式中的 return 其實(shí)結(jié)束的不是 hello() 本身,而是穿透了 hello() 結(jié)束了 main()。而示例2中是正常打印了所有的字符串,即被 return 結(jié)束的函數(shù)只是 hello(),main() 不受影響。而示例1為何是這樣執(zhí)行?其實(shí)原因也很簡(jiǎn)單!在介紹 inline 的時(shí)候我們提到過(guò),內(nèi)聯(lián)函數(shù)的函數(shù)類型參數(shù)會(huì)被展開鋪平,那示例1編譯后的代碼應(yīng)該如下所示:
inline fun hello(block: () -> Unit) {
println("Say Hello!")
block()
}
fun main() {
hello {
println("Bye!")
return
}
println("Continue")
}
// 實(shí)際編譯后
fun main() {
println("Bye!")
return
println("Continue")
}
可以看到,在編譯后,main() 中其實(shí)是有個(gè) return 的,那么 println("Continue") 就自然不會(huì)被執(zhí)行了。但是問(wèn)題就來(lái)了,如果我們?cè)?return 的時(shí)候都需要去查看原函數(shù)是否被 inline 標(biāo)記,這也太不科學(xué)了吧!所以在 Kotlin 中有一條規(guī)則:在 Lambda 表達(dá)式中不允許直接使用 return,除非這個(gè) Lambda 是內(nèi)聯(lián)函數(shù)的參數(shù)。 這樣規(guī)則就簡(jiǎn)單多了:
- 只有作為內(nèi)聯(lián)函數(shù)的參數(shù)的 Lambda 表達(dá)式中可以直接使用 return
- Lambda 表達(dá)式中的 return 結(jié)束的不是內(nèi)聯(lián)函數(shù)自身,而是內(nèi)聯(lián)函數(shù)的調(diào)用函數(shù)
4.2 內(nèi)聯(lián)函數(shù)中的間接調(diào)用
有這么一個(gè)需求,我們需要在UI線程中去執(zhí)行內(nèi)聯(lián)函數(shù)中的某個(gè)代碼塊,這個(gè)需求應(yīng)該不過(guò)分吧?常規(guī)寫法一般如下:
// 錯(cuò)誤示例
inline fun hello(block: () -> Unit) {
println("Say Hello!")
runOnUiThread {
block()
}
}
fun main() {
hello {
println("Bye!")
return
}
println("Continue")
}
這其實(shí)就是在內(nèi)聯(lián)函數(shù)中間接調(diào)用函數(shù)類型參數(shù),說(shuō)“間接”是因?yàn)楸緛?lái) block 的控制權(quán)是在 hello() 中,其外層是 hello(),這么一寫,控制權(quán)就被 runOnUiThread 奪走了,外層變成了 runOnUiThread。而如果此時(shí),我們?cè)?hello() 的 Lambda 表達(dá)式中加個(gè) return,那么就會(huì)出現(xiàn)一個(gè)問(wèn)題, return 無(wú)法結(jié)束 main()。因?yàn)樽裱?inline 規(guī)則,最后編譯出的代碼大致是這樣的:
// 編譯后
fun main() {
println("Say Hello!")
runOnUiThread {
println("Bye!")
return
}
}
非常明顯,return 結(jié)束的是 runOnUiThread 中的 Runnable 對(duì)象,而不是 main()。那這樣與之前的規(guī)則不就沖突了嗎?所以事實(shí)上,這種間接調(diào)用寫法是不被允許的,IDE 會(huì)給你一個(gè)刺眼的紅線。那如果我們一定要間接調(diào)用該怎么做呢?這時(shí)就輪到 crossinline 出場(chǎng)了,正確示例如下:
// 正確示例
inline fun hello(crossinline block: () -> Unit) {
println("Say Hello!")
runOnUiThread {
block()
}
}
fun main() {
hello {
println("Bye!")
}
}
我們直接給 block 加上關(guān)鍵字 crossinline,這樣就允許間接調(diào)用了。但是如果我們還想要在 Lambda 中加上 return 該怎么做呢?那對(duì)不起,Kotlin不支持!
基于上述內(nèi)容,我們可以對(duì) crossinline 進(jìn)行一個(gè)總結(jié)。crossinline 字面意思是交叉內(nèi)聯(lián),也是作用于內(nèi)聯(lián)函數(shù)的函數(shù)類型參數(shù)上,其用途就是強(qiáng)化函數(shù)類型參數(shù)的內(nèi)聯(lián)優(yōu)化,使之能被間接調(diào)用,并且被 crossinline 標(biāo)記的 Lambda 中不能使用 return。
5. 總結(jié)
inline、noinline、crossinline 使 Kotlin 中常用的三個(gè)關(guān)鍵字:
- inline: 內(nèi)聯(lián),用于修飾函數(shù)。被修飾的函數(shù)稱為內(nèi)聯(lián)函數(shù),內(nèi)聯(lián)函數(shù)中的代碼可以直接嵌入到調(diào)用處,從而減少方法棧的層級(jí)與函數(shù)類型對(duì)象的創(chuàng)建。
- noinline: 不內(nèi)聯(lián),用于修飾內(nèi)聯(lián)函數(shù)中的函數(shù)類型參數(shù),關(guān)閉對(duì)函數(shù)類型參數(shù)的內(nèi)聯(lián)優(yōu)化,從而擺脫 inline 帶來(lái)的「不能把函數(shù)類型參數(shù)當(dāng)對(duì)象使用」的限制。
- crossinline: 交叉內(nèi)聯(lián),用于修飾內(nèi)聯(lián)函數(shù)中的函數(shù)類型參數(shù),使函數(shù)類型參數(shù)能被間接調(diào)用。