likes
comments
collection
share

Kotlin中inline、noinline和corssinline到底是什么?

作者站长头像
站长
· 阅读数 17

本文基于Kotlin 1.7.0版本,目前Kotlin最新版本为1.8.22

相信大家在日常使用Kotlin的过程中,已经接触了很多inline函数,包括源码中也有很多很多方法使用了inline来修改某些方法,不知道是不是有种疑问,一个方法明明可以直接调用,为啥非要用inline来修饰呢?inline修改的方法参数中,竟然还有noinlinecrossinline关键字来修饰lambda。下面来详细说明下这三个关键字的作用和使用场景。

inline内联

inline修饰的方法叫做内联函数,它修饰的方法需要接收了一个或多个lambda表达式作为参数,

如果此方法参数没有lambda表达式,那么编译器将提醒你Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types,这个警告说明:此内联对性能的影响很小、微乎其微,内联适合的是具有函数类型的参数,所以编译器觉得此方法不适用于inline修饰。

我们来通过inline和非inline函数对比一下它的作用,两个方法实现相同的功能,然后看下最终编译器是如何调用两种函数。

// 此函数为内联函数,传入一个lambda表达式
inline fun inlineTest(block: () -> Unit) {
    println("inlineTest")
    block()
}

// 此函数为非内联函数,也是传入一个lambda表达式
fun test(block: () -> Unit) {
    println("test")
    block()
}

两个函数除了inline关键字修饰不同意外,其余都是一致的,接下来看看调用之后的输出是否一致,此处我们加上了测量函数执行时间的功能,以此更直观的观察inline带来的效果。

fun main() {
    println(measureTimeMillis {
        inlineTest {
            println("main inlineTest")
        }
    })

    println(measureTimeMillis {
        test {
            println("main test")
        }
    })
}

# 
inlineTest
main inlineTest
1
test
main test
9

inline函数执行时间为1,非inline函数执行时间为9

两个函数执行的效果是一致的,但是从输出日志中可以暗处,inline函数的执行时间要明显比非inline函数少,这也就是为什么官方推荐我们在有lambda参数的时候加上inline使其变为内联函数,那么inline到底有了什么魔方可以减少了函数调用的开销呢,下面通过将class反编译成Java来看看函数的具体调用。

public final class MainKt {
   public static final void main() {
    	// ①此处为inline函数的调用过程
      long start$iv = System.currentTimeMillis();
      System.out.println("inlineTest");
      System.out.println("main inlineTest");
      long var6 = System.currentTimeMillis() - start$iv;
      System.out.println(var6);

    	// ②此处为非inline函数的调用过程
      start$iv = System.currentTimeMillis();
      test((Function0)null.INSTANCE);
      var6 = System.currentTimeMillis() - start$iv;
      System.out.println(var6);
   }

   // ③在编译器中可以看到此处inline函数并没有调用的地方
   public static final void inlineTest(@NotNull Function0 block) {
      Intrinsics.checkNotNullParameter(block, "block");
      int $i$f$inlineTest = false;
      System.out.println("inlineTest");
      block.invoke();
   }

   public static final void test(@NotNull Function0 block) {
      Intrinsics.checkNotNullParameter(block, "block");
      System.out.println("test");
      block.invoke();
   }

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

在反编译的代码中标记了三处地方,顺着这三处就可以清晰的看出inline函数和常规函数的不同之处。

第一处①是inline函数的具体调用过程,从代码中可以看出,此处并没有直接调用inlineTest()这个方法,反而是直接将函数的内容拷贝到调用处,并且将lambda中的代码也一并拷贝过来了,直接减少了函数调用的开销;

第二处②是常规函数的调用过程,它是调用了test()方法,并且传入了一个Function对象,这个Function0就是我们lambda表达式

第三处③是需要在编译器中才能看出效果,在编译器中,我们可以看出teinlineTest()函数并没有调用者,而test()函数是在main()函数中有调用的地方。

这样我们就可以直观的感受到,inline修饰的函数也就是内联函数在调用的时候并非直接调用此函数本身,而是将函数内的代码直接拷贝到调用处。这样带来优势就是:减少函数调用带来的开销,提高程序的性能;消除lambda表达式带来的额外开销,避免创建额外的对象。

noinline

上面我们了解了inline内联函数的使用和优势,接着我们看一下配合inline使用的noinline,看名字大致可以猜测到,noinline就是非内联的意思,也就是表明被noinline修饰的参数强制不允许内联,此参数作为一个普通的函数引用传递,并且noinline必须搭配inline使用。下面还是通过代码来直观感受下noinline的作用。

fun main() {
    noinlineTest({
        println("main inline")
    }, {
        println("main noInline")
    })
}

// 定义一个内联函数,第一个参数可内联使用,第二个参数使用noinline修饰,强制不内联
inline fun noinlineTest(block: () -> Unit, noinline no: () -> Unit) {
    println("noinlineTest")
    block()
    no()
}

# log
noinlineTest
main inline
main noInline

noinlineTest({},{})函数为一个内联函数,两个lambda参数唯一不同的就是第二个参数被noinline修饰了,从log中可以看出,输出的信息在我们意料之中,也并不能看出noinline带来的不同之处,我们还是得反编译看下生成的代码到底变化了什么。

public final class NoinlineKt {
   public static final void main() {
    	// ① no参数为一个Function,直接实例化了
      Function0 no$iv = (Function0)null.INSTANCE;
    	// ② noinlineTest函数中打印的日志
      System.out.println("noinlineTest");
    	// ③ block函数内代码直接拷贝到这
      System.out.println("main inline");
      // ④ 执行no参数具体的代码
      no$iv.invoke();
   }

   public static final void noinlineTest(@NotNull Function0 block, @NotNull Function0 no) {
      Intrinsics.checkNotNullParameter(block, "block");
      Intrinsics.checkNotNullParameter(no, "no");
      int $i$f$noinlineTest = false;
      System.out.println("noinlineTest");
      block.invoke();
      no.invoke();
   }

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

反编译的代码中我们注释了四处地方,分别介绍了内联参数和noinline参数的执行步骤:

第一处①先实例化一个no参数,Kotlin的lambdaJava中对应的是Function对象;

第二处②直接拷贝内联函数noinlineTest()println()方法输出日志;

第三处③直接拷贝内联参数的输出日志方法,从此处可以看到block参数被内联了,它被拷贝到调用处;

第四处④执行了noinline参数的内部代码

从上面反编译的代码我们可以得出,noinline修饰的参数被强制非内联了,它还是会去调用内部的代码,而非直接拷贝内部代码到调用处,这就是noinline关键字的作用。

crossinline

crossinline相对于前面inlinenoinline来说,它使用的地方较少,个人的理解它的意思为强制内联的意思,它表示被修饰的lambda参数强制执行内联作用,一般我们见到的使用它的地方都是在内联函数中使用了lambda表达式,并且在此表达式调用了内联函数的lambda参数,此时如果不使用crossinline修饰参数,编译器会报错,下面我们通过代码来说明

inline fun crossinlineTest(block: () -> Unit, crossinline cross: () -> Unit) {
    println("crossinlineTest")
    thread {
      	// 编译器会在此处报错:Can't inline 'block' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'block'
        block()
        cross()
    }
}

上面就是一个内联函数使用crossinline的实例,我们在内联函数中使用thread{}开启一个线程,并在新的线程中调用block()cross()参数的执行,此时编译器在block()调用处就直接报错,告诉我们block在此种场景下不能直接内联,它有可能包含了非本地的return,这样我们就需要加上crossinline来修饰参数,从cross调用的情况就说明了它可以正常执行。

编译器为什么不允许我们在thread{}中直接执行block()参数呢?

因为内联函数在调用的时候是直接将代码拷贝到调用处的,所以存在block()中直接return的情况,他会执行返回到调用处并且不再执行调用处后续的代码,看个具体代码理解一下:

inline fun inlineReturn(block: () -> Unit) {
    block()
}

fun main() {
    inlineReturn {
        println("start")
        return
        // 此处将不会执行
        println("end")
    }
  	// 此处也不会执行
    println("main")
}

# log
start

看上面代码,我们直接在inlineReturn函数的block()中使用return来返回,结果它并非退出到内联函数,而是直接退出了main()函数,到这我们记住inline函数是可以直接使用return来做出返回操作。

下面我们再看看crossinline关键字的作用:

inline fun crossinlineReturn(crossinline block: () -> Unit) {
    block()
}

fun main() {
    crossinlineReturn {
        println("start")
      	// 此处编译器会直接报错
        return
        println("end")
    }
    println("main")
}

crossinlineReturn和上面inlineReturn函数基本一致,只是block参数使用了crossinline修饰,此时我们如果还想使用return来操作返回,编译器会直接给出报错提示,告诉我们此时不可以使用return,它需要指定返回的目的地,需要采用return@crossinlineReturn这样的形式,告诉编译器只是退出到内联函数,并非直接退出main()函数,而且它的输出为:

# log
start
main

通过crossinline就可以禁止在内联的lambda表达式中使用return操作了。

好了,到这为止我们已经将inlinenoinlinecrossinline三者的关系及其用法、效果都介绍完了,如果你有收获帮忙点个关注吧,欢迎评论区输出不同的看法和认为不正确的地方,ths!

转载自:https://juejin.cn/post/7244498421284945981
评论
请登录