Kotlin总结之内联函数

在Kotlin 中使用 Lambda表达式会带来一些额外的开销。但可以通过内联函数优化。

一. 优化Lambda开销

在Kotlin中每次声明一个Lambda表达式,就会在字节码中产生一个匿名类。该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的对象。可想而知,Lambda虽然简洁,但是会增加额外的开销。Kotlin 采用内联函数来优化Lambda带来的额外开销。

1.1 invokedynamic

Java如何解决优化Lambda的问题的呢?与Kotlin这种在编译期通过硬编码生成Lambda转换类的机制不同,Java在SE7之后,通过invokedynamic技术实现了在运行期间才产生相应的翻译代码。在invokedynamic被首次调用的时候,就会触发产生一个匿名内部类来替换中间码invokedynamic,后续的调用会直接采用这个匿名类的代码。

这样做的好处是:

1.由于具体的转换实现是在运行时产生的,在字节码中能看到的只有一个固定的invokedynamic,所以需要静态生成的类的个数以及字节码大小显著减少。

2.与编译时写死在字节码中的策略不同,利用invokedynamic可以把实际的翻译策略隐藏在JDK库的实现,提高了灵活性,在确保向后兼容性的同时,后期可以继续对翻译策略不断优化升级。

3.JVM天然支持类针对该方式对Lambda表达式的翻译和优化,开发者不必考虑这个问题。

1.2内联函数

invokedynamic虽然不错,但是Kotlin需要兼容Android最主流的Java版本SE6,这导致Kotlin无法使用invokedynamic来解决android平台Lambda开销的问题。所以Kotlin使用内联函数来解决这个问题,在Kotlin中使用inline关键字来修饰函数,这些函数就成了内联函数。它们的函数体在编译的时期被嵌入到每一个调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。

2.内联函数具体语法

声明一个高阶函数payFoo,可以接收一个类型为()->Unit的Lambda,然后在main函数中调用它。

fun main() {
    payFoo {
        println("write kotlin...")
    }
}
fun payFoo(block: () -> Unit) {
    println("before block")
    block()
    println("end block")
}

通过字节码反编译的相关Java代码。

public final class InlineDemoKt {
   public static final void main() {
      payFoo((Function0)null.INSTANCE);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void payFoo(@NotNull Function0 block) {
      Intrinsics.checkParameterIsNotNull(block, "block");
      String var1 = "before block";
      System.out.println(var1);
      block.invoke();
      var1 = "end block";
      System.out.println(var1);
   }
}

可以发现调用payFoo后就会产生一个Function0类型的block类,然后通过invoke 方法来执行,这会增加额外的生成类和函数调用开销。

现在通过inline修饰payFoo函数:

fun main() {
    payFoo {
        println("write kotlin...")
    }
}
inline fun payFoo(block: () -> Unit) {
    println("before block")
    block()
    println("end block")
}
public final class InlineDemoKt {
   public static final void main() {
      int $i$f$payFoo = false;
      String var1 = "before block";
      System.out.println(var1);
      int var2 = false;
      String var3 = "write kotlin...";
      System.out.println(var3);
      var1 = "end block";
      System.out.println(var1);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void payFoo(@NotNull Function0 block) {
      int $i$f$payFoo = 0;
      Intrinsics.checkParameterIsNotNull(block, "block");
      String var2 = "before block";
      System.out.println(var2);
      block.invoke();
      var2 = "end block";
      System.out.println(var2);
   }
}

可以发现通过inline修饰的函数,其函数体代码被调用的Lambda代码都粘贴到了相应调用的位置。试想一下,如果这是一个工程中公共的方法,或者被嵌套在一个循环调用的逻辑体中,这个方法必然会被调用多次。通过inline语法,可以彻底消除这种额外调用,从而节约了开销。

内联函数典型的一个应用场景就是Kotlin的集合类。如果你看过 Kotlin的集合类API文档或者源码实现,可以发现,集合函数式API,如map、filter都被定义成了内联函数。

/**
 * Returns a list containing the results of applying the given [transform] function
 * to each element in the original array.
 */
public inline fun <T, R> Array<out T>.map(transform: (T) -> R): List<R> {
    return mapTo(ArrayList<R>(size), transform)
}
/**
 * Returns a list containing only elements matching the given [predicate].
 */
public inline fun <T> Array<out T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

因为上面的方法都接收Lambda作为参数,同时需要对集合元素进行遍历操作,所以会定义为内联函数。

内联函数不是万能的,以下情况避免使用内联函数:

1.由于JVM对普通函数已经能够根据实际情况智能地判断是否进行内联优化,所以我们并不需要对其使用Kotlin的inline语法,那只会让字节码变得更加复杂。

2.尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量。

3.一旦一个函数被定义为内联函数,便不能获取闭包类的私有成员,除非你把它们声明为internal。

下面就是错误写法,本质是private修饰的a没有get 方法。

class TestPay {
      private var a = 1  //错误写法,会报错

     inline fun printNumber() {
        println(a)
    }
}

 

二.noinline :避免参数被内联

如果一个函数的开头加上inline修饰符,那么它的函数体以及Lambda参数都会被内联。然而现实中的情况比较复杂,有一种可能是函数需要接收多个参数,但我们只想对其中部分Lambda参数内联,其他的则不内联,应该如何处理?

Kotlin引入了noinline关键字,可以加在不想要内联的参数开头,该参数便不会有内联效果。

fun main() {
    payFoo({
        println("I am inlined...")
    }, {
        println("I am not inlined...")
    })
}

inline fun payFoo(block1: () -> Unit, noinline block2: () -> Unit) {
    println("before block")
    block1()
    block2()
    println("end block")
}

before block
I am inlined...
I am not inlined...
end block

可以发现block1被内联了,block2没有被内联。

public final class NoinlineDemoKt {
   public static final void main() {
      Function0 block2$iv = (Function0)null.INSTANCE;
      int $i$f$payFoo = false;
      String var2 = "before block";
      System.out.println(var2);
      int var3 = false;
      //可以发现block1被内联了。
      String var4 = "I am inlined...";
      System.out.println(var4);
      //block2没有被内联
      block2$iv.invoke();
      var2 = "end block";
      System.out.println(var2);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void payFoo(@NotNull Function0 block1, @NotNull Function0 block2) {
      int $i$f$payFoo = 0;
      Intrinsics.checkParameterIsNotNull(block1, "block1");
      Intrinsics.checkParameterIsNotNull(block2, "block2");
      String var3 = "before block";
      System.out.println(var3);
      block1.invoke();
      block2.invoke();
      var3 = "end block";
      System.out.println(var3);
   }
}

 从上面的代码中可以看出,payFoo函数中block2函数在带上noinline之后,反编译后的Java代码中没有将其函数体代码在调用处进行替换。

三.非局部返回

Kotlin中内联函数除了优化Lambda开销之外,还带来了非局部返回和具体化参数类型。

1.Kotlin如何支持非局部返回

首先看常见的局部返回的例子

fun main() {
    payFoo()
}

fun localReturn() {
    return
}

fun payFoo() {
    println("before local return")
    localReturn()
    println("after local return")
    return
}

before local return
after local return

从上面代码可以发现,localReturn执行之后,其函数体中的return只会在该函数的局部生效,所以localReturn()之后的println函数依旧生效。

如何把函数换成Lambda版本,(存在问题的写法)

fun main() {
    payFoo { return }
}

fun payFoo(returning: () -> Unit) {
    println("before local return")
    returning()
    println("after local return")
    return
}

'return' is not allowed here

从上面的代码可以发现报错了,在Kotlin中,正常情况下,Lambda表达式不允许存在return关键字。

通过内联函数修改

fun main() {
    payFoo { return }
}

inline fun payFoo(returning: () -> Unit) {
    println("before local return")
    returning()
    println("after local return")
    return
}

before local return

编译通过,但结果与局部返回不同,Lambda的return执行之后直接让foo函数退出了执行。因为内联函数payFoo的函数体及参数Lambda会直接替代具体的调用,所以实际产生的代码中,return相当于是直接暴露在main函数中的,所以returning之后的代码就不会执行了,这就是非局部返回。

public final class Testinline3Kt {
   public static final void main() {
      int $i$f$payFoo = false;
      String var1 = "before local return";
      System.out.println(var1);
      int var2 = false;
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void payFoo(@NotNull Function0 returning) {
      int $i$f$payFoo = 0;
      Intrinsics.checkParameterIsNotNull(returning, "returning");
      String var2 = "before local return";
      System.out.println(var2);
      returning.invoke();
      var2 = "after local return";
      System.out.println(var2);
   }
}

使用标签实现Lambda非局部返回

另一种等效的方式,是通过标签利用@符号来实现Lambda非局部返回。可以在不声明inline修饰符的情况下,实现相同效果。

fun main() {
    payFoo2 { return@payFoo2 }
}

fun payFoo2(returning: () -> Unit) {
    println("before local return")
    returning()
    println("after local return")
    return
}

before local return
after local return

非局部返回尤其在循环控制中特别有用,比如Kotlin的forEach接口,它接收一个Lambda参数,由于它也是一个内联函数,所以可以直接在它调用的Lambda中执行return退出上一层的程序。

fun main() {
    println(hasZero(listOf(0,2,3)))
}

fun hasZero(list: List<Int>): Boolean {
    list.forEach {
        if (it == 0) return true //直接返回结果
    }
    return false
}

true

crossinline

非局部返回在某些场合下非常有用,但可能存在风险,内联的函数所接收的Lambda参数常常来自于上下文的其他地方。为了避免带有return的Lambda参数产生破坏,可以使用crossinline 关键字来修饰该参数。

fun main() {
    payFoo3 {
        return
    }
}

inline fun payFoo3(crossinline returning: () -> Unit) {
    println("before local return")
    returning()
    println("after local return")
    return
}

'return' is not allowed here

四.具体化参数类型

内联函数可以帮助我们实现具体化参数类型,Kotlin与Java一样,由于运行时的类型擦除,我们不能直接获取一个参数的类型。然而,由于内联函数会直接在字节码中生成相应的函数体实现,这时候反而可以获得参数的具体类型。

使用reified修饰符来实现这一效果。

fun main() {
    getType<Int>()
}


inline fun <reified T> getType() {
    println(T::class)
}

class java.lang.Integer (Kotlin reflection is not available)

这个特性在android开发中也特别有用,在Java中,需要调用startActivity时,通常需要把具体的目标视图类作为一个参数。但是在Kotlin中,可以使用reified来进行简化。

import android.app.Activity
import android.content.Intent

inline fun <reified T : Activity> Activity.startActivity() {
    startActivity(Intent(this, T::class.java))
}

参考<<Kotlin核心编程>> 

 

 

 

 

 

 

 

发布了179 篇原创文章 · 获赞 175 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/zhangying1994/article/details/104712148