美文网首页Kotlin
Kotlin Vocabulary | 内联函数的原理与应用

Kotlin Vocabulary | 内联函数的原理与应用

作者: 谷歌开发者 | 来源:发表于2020-06-22 10:21 被阅读0次

    我们的项目里常常会创建一些 Util 类,用于分类整理那些会在许多地方用到的小型函数 (也称实用函数),如果这类函数接收了另一个函数作为参数,则可能会造成一些额外的对象分配,通过使用 inline 关键字,您可以避免这种情况并提升应用性能。接下来我们就来看一看,当您把一个函数作为参数传递时发生了什么、inline 关键字背后做了哪些工作,以及使用内联函数 (inline function) 时的注意事项。

    函数调用——工作原理

    我们在应用中常常要用到 SharedPreferences,现在假设您为了减少每次向 SharedPreferences 中写入内容时产生的模板代码,实现了以下实用函数:

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

    然后,您就可以用这个方法保存一个字符串 "token" :

    
    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 指令。所以虽然我们没有调用任何其他对象的构造函数,却还是创建出了一个新的对象:

    NEW com/example/inlinefun/PreferencesManager$saveToken$1
    

    为了便于理解,让我们查看一下反编译后的代码。我们的 saveToken 函数反编译后的代码如下 (我做了注释和格式化):

    
    /* 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 代码 (做了注释和格式化):

    
    /* 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 关键字,编译器会将内联函数的内容复制到调用处,从而避免了创建新的函数对象。

    应该在哪些地方使用 inline 标记?

    ⚠️ 如果您试图标记为内联函数的函数,并没有接收另一个函数作为参数,您将无法获得明显的性能提升,而且 IDE 甚至会建议您移除 inline 标记:


    ⚠️ 因为 inline 关键字可能会增加代码的生成量,所以一定要避免内联大型函数。举例来说,如果去查看 Kotlin 标准库中的内联函数,您会发现它们大部分都只有 1 - 3 行。

    ⚠️ 不要内联大型函数!

    ⚠️ 使用内联函数时,您不能持有传入的函数参数对象的引用,也不能将传入的函数参数对象传递给另一个函数——这么做将会触发编译器报错,它会说您非法使用内联参数 (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 Vocabulary | 内联函数的原理与应用

        本文链接:https://www.haomeiwen.com/subject/onztfktx.html