Kotlin的inline、noinline、crossinline全面分析1

前言

说起内联函数,熟悉Kotlin的Android开发肯定使用过,比如我们常见的apply函数:

public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
复制代码

我们也大概知道这个inlie的作用是啥,不过一般都没有仔细思考过为什么,以及它还有2个好兄弟oninline和crossinline的作用,这篇文章我就准备从最简单的地方,挨个举例来说清楚这些。

正文

首先就是必须了解Kotlin的lambda以及函数类型,可以看我之前的文章:

# Kotlin lambda,有你想了解的一切

这里有说了一个重要概念,就是Kotlin的高阶函数其实就是实现FunctionN的一个实例,还有就是Kotlin的lambda作为实参传递给参数时,也会创建匿名内部类以及调用invoke方法,我们这里来逐步深入。

Android Studio反编译Kotlin代码

这个是AS的很好用的一个功能,当我们写了一个Kotlin文件,但是我们想看它编译成Java是什么样子的,可以直接使用AS来进行。

比如下面代码:

image.png

如下显示Kotlin字节码:

image.png

然后点击这个反编译按钮:

image.png

然后便生成了对应的Java文件:

image.png

比如这里我们就可以看出Kotlin代码中的lambda这里创建了一个OnClickListener的实例对象,所以当不熟悉的Kotlin代码,把它反编译成对应的Java代码将会好容易理解很多。

lambda作为实参传入Kotlin高阶函数

这里还是复习一下Kotlin高阶函数中把lambda作为实参会发生什么,比如下面代码:

//在Kt文件中定义一个高阶函数
fun lambdaFun(action: (() -> Unit)){
    Log.i("zyh", "testLambdaFun: 调用前")
    action()
    Log.i("zyh", "testLambdaFun: 调用后")
}
复制代码

调用上面高阶函数:

fun testHello1(){
    lambdaFun {
        Log.i("zyh", "testLambdaFun: 正在调用")
    }
}
复制代码

给转换成Java代码:

public final class TestFun {
   public final void testHello1() {
       //把lambda转换成了Function0的实例
      TestInlieKt.lambdaFun((Function0)null.INSTANCE);
   }
}
复制代码

会发现这里没啥问题,把lambda转换成匿名内部类实现合情合理,但是当有很多的地方都这样写:

//调用多次lambda
fun testHello1(){
    for (i in 0 .. 10){
        lambdaFun {
            Log.i("zyh", "testLambdaFun: 正在调用")
        }
    }
}
复制代码

反编译结果:

public final class TestFun {
   public final void testHello1() {
      int var1 = 0;
      //创建了多个匿名内部类实例
      for(byte var2 = 10; var1 <= var2; ++var1) {
         TestInlieKt.lambdaFun((Function0)null.INSTANCE);
      }

   }
}
复制代码

这里就会发现有问题了,当匿名内部类过多时,会导致内存增加,这里也就是inline关键字出现的原因。

inline关键字

inline关键字可以修饰函数,然后函数称之为内联函数,当函数是内联函数时,函数内部的函数体以及函数类型参数都会被“内联”到调用地方。

这里比较难理解,要理解2点,一个是函数内部的函数体的内联,一个传递进来的lambda表达式的内联,这个十分重要,在后面细说。

为了理解,我们还是举个简单例子:

//定义一个非高阶函数
fun normalFun(){
    Log.i("zyh", "testLambdaFun: 调用前")
    Log.i("zyh", "testLambdaFun: 调用后")
}
复制代码

调用地方,然后进行反编译:

fun testHello(){
    normalFun()
}
复制代码
//反编译
public final class TestFun {
   public final void testHello() {
      TestInlieKt.normalFun();
   }
}
复制代码

这里就是正常调用,当把normalFun定义成inline函数:

//普通函数添加inline
inline fun normalFun(){
    Log.i("zyh", "testLambdaFun: 调用前")
    Log.i("zyh", "testLambdaFun: 调用后")
}
复制代码

然后进行调用,然后反编译:

//反编译代码
public final class TestFun {
   public final void testHello() {
      int $i$f$normalFun = false;
      Log.i("zyh", "testLambdaFun: 调用前");
      Log.i("zyh", "testLambdaFun: 调用后");
   }
}
复制代码

看到这里是不是有一种豁然开朗的感觉,这里直接把normalFun函数内的逻辑直接复制到调用地方,很nice。

注意这里的操作是由Kotlin编译器干的事,所以我们可以不用探讨。

假如这里只有这个对普通函数的效果,那未免没有什么意思,顶多也就让调用站少了一层,但是让编译器干了这么多事,肯定不划算,真正的亮点在于高阶函数。

inline修饰高阶函数

inline不仅可以“铺平内联”函数内的代码,还可以“铺平内联”函数类型的参数,这才是关键,比如下面代码:

//高阶函数添加inline
inline fun lambdaFun(action: (() -> Unit)){
    Log.i("zyh", "testLambdaFun: 调用前")
    action()
    Log.i("zyh", "testLambdaFun: 调用后")
}
复制代码

然后进行调用,进行反编译:

//反编译代码
public final class TestFun {
   public final void testHello() {
      int $i$f$lambdaFun = false;
      Log.i("zyh", "testLambdaFun: 调用前");
      int var2 = false;
      Log.i("zyh", "testLambdaFun: 调用中");
      Log.i("zyh", "testLambdaFun: 调用后");
   }
}
复制代码

会惊喜地发现,这里反编译的代码没有匿名内部类的影子,这也就达到了我们的目的,即使这里调用多个lambda,假如被调用的函数被声明为了inline,那也不会创建出多个无用的类,可以大大减小内存使用。

总结

其实内联函数很关键,它解决了使用lambda方便的同时创建过多的匿名内部类的弊端,这里我更喜欢把被调用的inline函数的变化称为复制铺平,也就是把函数体的代码和函数类型参数给赋值铺平到调用地方,当然这种叫法只是个人理解。

既然赋值铺平很好用,那是不是所有inline函数的函数类型参数都可以复制铺平呢 我们下篇文章再说,也就是noinlien和crossinline的使用。

# Kotlin的inline、noinline、crossinline全面分析2

おすすめ

転載: juejin.im/post/7050664159247073316