Kotlin Vocabulary | 內聯函數的原理與應用

咱們的項目裏經常會建立一些 Util 類,用於分類整理那些會在許多地方用到的小型函數 (也稱實用函數),若是這類函數接收了另外一個函數做爲參數,則可能會形成一些額外的對象分配,經過使用 inline 關鍵字,您能夠避免這種狀況並提高應用性能。接下來咱們就來看一看,當您把一個函數做爲參數傳遞時發生了什麼、inline 關鍵字背後作了哪些工做,以及使用內聯函數 (inline function) 時的注意事項。

函數調用——工做原理

咱們在應用中經常要用到 SharedPreferences,如今假設您爲了減小每次向 SharedPreferences 中寫入內容時產生的模板代碼,實現瞭如下實用函數:html

fun SharedPreferences.edit(
    commit: Boolean = false,
    action: SharedPreferences.Editor.() -> Unit
) {
    val editor = edit()
    action(editor)
    if (commit) {
        editor.commit()
    } else {
        editor.apply()
    }
}
複製代碼

而後,您就能夠用這個方法保存一個字符串 "token" :android

private const val KEY_TOKEN = 「token」

class PreferencesManager(private val preferences: SharedPreferences){
    fun saveToken(token: String) {
        preferences.edit { putString(KEY_TOKEN, token) }
    }
}
複製代碼

接下來咱們看看,preferences.edit 被調用時其背後發生了什麼。若是咱們查看 Kotlin 字節碼 (Tools > Kotlin > Decompiled Kotlin to Java),就能看到這裏調用了 NEW 指令。因此雖然咱們沒有調用任何其餘對象的構造函數,卻仍是建立出了一個新的對象:bash

NEW com/example/inlinefun/PreferencesManager$saveToken$1
複製代碼

爲了便於理解,讓咱們查看一下反編譯後的代碼。咱們的 saveToken 函數反編譯後的代碼以下 (我作了註釋和格式化):app

/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */
public final void saveToken(@NotNull final String token) {

    // 咱們定義的修改 SharedPreferences 的擴展方法被調用了
    PreferenceManagerKt.edit$default(
        this.preferences, // SharedPreferences 實例對象
        false,// commit 標記的默認值
        (Function1)(new Function1() { // 爲 action 參數建立了新的 Function 對象
            // $FF: synthetic method
            // $FF: bridge method
            public Object invoke(Object var1) {
                this.invoke((Editor)var1);
                return Unit.INSTANCE;
            }
            public final void invoke(@NotNull Editor $this$edit) {
                Intrinsics.checkParameterIsNotNull($this$edit, "$receiver");
                $this$edit.putString("token", token); // 咱們 action 參數中的實現
            }
        }), 1, (Object)null);
}
複製代碼

每一個高階函數都會形成函數對象的建立和內存的分配,從而帶來額外的運行時開銷。函數

內聯函數——工做原理

爲了提高咱們應用的性能,咱們能夠經過使用 inline 關鍵字,來減小函數對象的建立:性能

inline fun SharedPreferences.edit(
    commit: Boolean = false,
    action: SharedPreferences.Editor.() -> Unit
) { … }
複製代碼

如今,Kotlin 字節碼中已經不包含任何 NEW 指令的調用了,下面是 saveToken 方法反編譯出的 Java 代碼 (作了註釋和格式化):ui

/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */
public final void saveToken(@NotNull String token) {
  // SharedPreferences.edit 函數中的內容
  SharedPreferences $this$edit$iv = this.preferences;
  boolean commit$iv = false;
  int $i$f$edit = false;
  Editor editor$iv = $this$edit$iv.edit();
  Intrinsics.checkExpressionValueIsNotNull(editor$iv, "editor");
  int var7 = false;
  
  // action 參數中實現的內容
  editor$iv.putString("token", token);
  
  // SharedPreferences.edit 函數中的內容
  editor$iv.apply();
}
複製代碼

因爲使用了 inline 關鍵字,編譯器會將內聯函數的內容複製到調用處,從而避免了建立新的函數對象。this

應該在哪些地方使用 inline 標記?

⚠️ 若是您試圖標記爲內聯函數的函數,並無接收另外一個函數做爲參數,您將沒法得到明顯的性能提高,並且 IDE 甚至會建議您移除 inline 標記:google

⚠️ 由於 inline 關鍵字可能會增長代碼的生成量,因此必定要 避免內聯大型函數。舉例來講,若是去查看 Kotlin 標準庫中的內聯函數,您會發現它們大部分都只有 1 - 3 行。

⚠️ 不要內聯大型函數!spa

⚠️ 使用內聯函數時,您不能持有傳入的函數參數對象的引用,也不能將傳入的函數參數對象傳遞給另外一個函數——這麼作將會觸發編譯器報錯,它會說您非法使用內聯參數 (inline-parameter)。

舉個例子,咱們修改一下 edit 方法和 saveToken 方法。edit 方法得到了一個新的函數參數,並在隨後將其傳遞給了另外一個函數。saveToken 方法則會在新的函數參數中更新一個隨意設置的模擬變量:

fun myFunction(importantAction: Int.() -> Unit) {
    importantAction(-1)
}

inline fun SharedPreferences.edit(
    commit: Boolean = false,
    importantAction: Int.() -> Unit = { },
    action: SharedPreferences.Editor.() -> Unit
) {
    myFunction(importantAction)
    ...
}
...
fun saveToken(token: String) {
    var dummy = 3
    preferences.edit(importantAction = { dummy = this}) {
         putString(KEY_TOKEN, token)
    }
}
複製代碼

咱們將會看到 myFunction(importantAction) 產生了一個錯誤:

當遇到這種狀況時,基於您函數的不一樣,有下面這些解決方案:

第一種狀況: 若是您的函數有多個函數參數,可是您須要持有其中某個的引用時,您能夠將對應的參數標記爲 noinline

經過使用 noinline,編譯器就只會爲對應函數建立新的 Function 對象,其他的則依舊會被內聯。

咱們的 edit 函數如今會變成下面這樣:

inline fun SharedPreferences.edit(
    commit: Boolean = false,
    noinline importantAction: Int.() -> Unit = { },
    action: SharedPreferences.Editor.() -> Unit
) {
    myFunction(importantAction)
    ...
}
複製代碼

若是咱們去查看字節碼,將會看到這裏出現了一個 NEW 指令的調用:

NEW com/example/inlinefun/PreferencesManager$saveToken$1
複製代碼

在反編譯後的代碼中,咱們會看到以下內容 (加入了註釋):

/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */
public final void saveToken(@NotNull String token) {
   // saveToken 方法中的功能
   final IntRef x = new IntRef();
   x.element = 3;
   
   // 內聯 edit 方法中的功能
   SharedPreferences $this$edit$iv = this.preferences;
 
   // noinline 函數聲明致使 new Function 被調用
   Function1 importantAction$iv = (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 $receiver) {
            // saveToken 的功能
           x.element = $receiver;
        }
   });
  
   // 內聯 edit 方法中的功能
   boolean commit$iv = false;
   int $i$f$edit = false;
   PreferenceManagerKt.myFunction(importantAction$iv);
   Editor editor$iv = $this$edit$iv.edit();
   Intrinsics.checkExpressionValueIsNotNull(editor$iv, "editor");
   int var9 = false;
   editor$iv.putString("token", token);
   editor$iv.apply();
}
複製代碼

第二種狀況: 若是您的函數只接收一個函數做爲參數,那麼就乾脆不要使用 inline。若是您執意使用 inline 關鍵字,就必須將參數標記爲 noinline,可是這麼一來,內聯此方法的性能優點微乎其微。

爲了減小 lambda 表達式帶來的額外內存分配,建議您使用 inline 關鍵字!只需注意,標記對象最好是接收一個 lambda 表達式做爲參數的小型函數。若是您須要持有 (做爲內聯函數參數的) lambda 表達式的引用,或者想要將它做爲參數傳遞給另外一個函數,使用 noinline 關鍵字標記對應參數便可。節約開銷,從使用 inline 作起!

點擊這裏瞭解更多關於用 Kotlin 進行 Android 開發的相關資料

相關文章
相關標籤/搜索