Kotlin的inline、noinline和crossinline关键字 内联

一、inline
inline翻译成中文的意思就是内联,在kotlin里面inline被用来修饰函数,表明当前函数在编译时是以内嵌的形式进行编译的,从而减少了一层函数调用栈:

inline fun fun1() {
    Log.i("tag", "1")
}
 
//调用
fun mainFun() {
    fun1()
}
 
//实际编译的代码
fun mainFun() {
    Log.i("tag", "1")
}


这样写的一点好处就是调用栈会明显变浅:

但是这个好处对应用程序的优化影响非常小,几乎可以忽略不计。甚至可能会由于多处调用代码重复编译导致编译字节码膨胀从而造成包体积变大的问题,这就得不偿失。所以inline关键字的正确使用场景并不是如上图所示。

我们都知道kotlin允许函数可以作为另一个函数的入参对象进行调用,在实际调用处入参的函数体会被创建为一个对象:
 

fun fun1(doSomething: () -> Unit) {
    Log.i("tag", "1")
    doSomething()
}
 
//调用
fun mainFun() {
    fun1 {
        Log.i("tag", "2")
    }
}
 
//实际编译的代码
fun mainFun() {
    val f = object: Function0<Unit> {
        override fun invoke() {
            Log.i("tag", "2")
        }
    }
    fun1(f)
}

 

一般情况下上图所示的调用逻辑并没有什么问题,创建一个小对象并不会对性能造成什么影响,但是如果我们将fun1放入for循环中呢:

fun mainFun() {
    for (i in 0..1000) {
        fun1 {
            Log.i("tag", "2")
        }
    }
}


那么在短时间内就会在mainFun函数中循环创建1000个f对象,这样应用进程的内存会瞬间飙升并造成某些性能上的严重问题,这就类似于为什么不让在onDraw函数中创建局部对象。而作为fun1函数的创建者,我们无法知道调用者会在什么场景以及时机去调用fun1函数,一旦出现上述重复创建大量函数对象的场景那么就会有严重的性能问题,而且这也是kotlin高阶函数的一个性能隐患。所以,基于这个问题kotlin提供了inline关键字来解决。

inline关键字可以将函数体内部的代码内联到调用处,甚至还可以将函数体内部的内部的代码也内联过去,而这个内部的内部的指的就是函数内部的函数类型的参数:
 

inline fun fun1(doSomething: () -> Unit) {
    Log.i("tag", "1")
    doSomething()
}
 
//调用
fun mainFun() {
    for (i in 0..1000) {
        fun1 {
            Log.i("tag", "2")
        }
    }
}
 
//实际编译的代码
fun mainFun() {
    for (i in 0..1000) {
        Log.i("tag", "1")
        Log.i("tag", "2")
    }
}

这样就避免了函数类型的参数所造成的临时函数对象的创建,我们就可以在界面高频刷新、大量循环的场景下放心调用fun1函数了。

总的来说,inline关键字让函数以内联的方式进行编译避免创建函数对象来处理kotlin高阶函数的天然性能缺陷。同时,之前的文章中提到的kotlin的泛型实化,也是利用了inline关键字可以内嵌函数代码的特性而衍生出来的全新功能。

二、noinline

顾名思义,noinline的意思就是不内联,这个关键字只能作用于内联高阶函数的某个函数类型的参数上,表明当前的函数参数不参与高阶函数的内联:

inline fun fun1(doSomething1: () -> Unit, noinline doSomething2: () -> Unit) {
    Log.i("tag", "1")
    doSomething1()
    doSomething2()
}
 
//调用
fun mainFun() {
    fun1({ Log.i("tag", "2") }, { Log.i("tag", "3") })
}
 
//实际编译的代码
fun mainFun() {
    Log.i("tag", "1")
    Log.i("tag", "2")
    ({Log.i("tag", "3")}).invoke()
}

但是这个关键字有什么用呢?

在kotlin中高阶函数的函数类型的参数我们可以直接当做一个函数去调用,但是函数类型的参数终究还是一个对象,既然是一个对象那么我们就可以以对象的形式去使用,就比如说作为函数返回值进行返回:

inline fun fun1(doSomething1: () -> Unit, doSomething2: () -> Unit): () -> Unit {
    Log.i("tag", "1")
    doSomething1()
    doSomething2()
    return doSomething2//这里编译器会提示报错,因为作为内联函数的函数类型参数,它已经不能作为对象去使用了。如果不加inline关键字这里就是正确的
}

我们知道内联函数的函数类型参数是不会再创建函数对象的,也就是说它只是一个函数体而不是一个函数对象,所以它无法被当做一个对象那样作为返回值进行返回。

所以当我们在内联函数里面真的需要将一个函数类型的参数作为对象去使用时就需要noinline关键字去修饰它:

inline fun fun1(doSomething1: () -> Unit, noinline doSomething2: () -> Unit): () -> Unit {
    Log.i("tag", "1")
    doSomething1()
    doSomething2()
    return doSomething2//编译正常
}


三、crossinline
crossinline的含义字面上可以理解为对inline做局部加强内联

我们先来看一个场景:
 

inline fun fun1(doSomething1: () -> Unit){
    Log.i("tag", "1")
    doSomething1()
}
 
//调用
fun mainFun() {
    fun1 {
        Log.i("tag", "2")
        return//按照一般的原则,这里的return结束的应该是fun1函数体中的逻辑,就是说Log.i("tag", "3")会被执行到。但是fun1作为内联函数会在编译时被完全铺平,这里return结束的就是最外面mainFun函数的逻辑,Log.i("tag", "3")就不会被执行到。
    }
    Log.i("tag", "3")
}
 
//实际编译的代码
fun mainFun() {
    Log.i("tag", "1")
    Log.i("tag", "2")
    return
    Log.i("tag", "3")
}

按照一般的原则,这里的return结束的应该是fun1函数体中的逻辑,就是说Log.i("tag", "3")会被执行到。但是fun1作为内联函数会在编译时被完全铺平,这里return结束的就是最外面mainFun函数的逻辑,Log.i("tag", "3")就不会被执行到。那我们在函数类型参数的lambda表达式中执行的return到底结束的是当前函数还是最外层的函数完全要看fun1到底是不是内联函数,这就让我们代码敲得很难受。为此,kotlin提出了一个新规定:lambda表达式中不允许直接使用return,除非是内联函数的函数类型参数的lambda表达式,并且它结束的是最外层的函数逻辑。同时其他场景下的lambda表达式中可以通过return@label的形式来显示指定return结束的代码作用域:
 

fun fun1(doSomething1: () -> Unit){
    Log.i("tag", "1")
    doSomething1()
}
 
fun mainFun() {
    fun1 {
        Log.i("tag", "2")
        //return//直接return在这里是不被允许的,因为fun1不是内联函数
        return@fun1//return@label的形式
    }
    Log.i("tag", "3")
}

再来看一种场景:

inline fun fun1(doSomething1: () -> Unit){
    Log.i("tag", "1")
    Runnable {
        doSomething1()//这里会编译报错
    }
}
 
//调用
fun mainFun() {
    fun1 {
        Log.i("tag", "2")
        return
    }
    Log.i("tag", "3")
}

我们在内联函数fun1中将函数类型参数doSomething1放到了子线程里面去执行,这样doSomething1和fun1就属于间接调用的关系,那mainFun函数中的return结束的到底是Runnable子线程中的逻辑还是mainFun函数中的逻辑呢?所以kotlin是不允许直接这样写的,但是我确实有需求要这样在内联函数中间接调用函数类型的参数怎么办?kotlin为此又新增了一条规定:内联函数中不允许类似上述问题中对函数类型参数的间接调用,除非该函数类型参数被crossinline关键字修饰:(这就是局部加强内联的含义)

inline fun fun1(crossinline doSomething1: () -> Unit){
    Log.i("tag", "1")
    Runnable {
        doSomething1()
    }
}
 
//调用
fun mainFun() {
    fun1 {
        Log.i("tag", "2")
        return//这里会编译报错
    }
    Log.i("tag", "3")
}

如上图所示,crossinline可以到达间接调用函数类型参数的目的,但是并没有解决“mainFun函数中的return结束的到底是Runnable子线程中的逻辑还是mainFun函数中的逻辑呢?”这一严重问题,于是kotlin干脆在间接调用的情况下内联函数的函数类型参数的lambda表达式不允许直接使用return,即下面这两个规定不可以共存:

1.lambda表达式中不允许直接使用return,除非是内联函数的函数类型参数的lambda表达式,并且它结束的是最外层的函数逻辑

2.内联函数中不允许类似上述问题中对函数类型参数的间接调用,除非该函数类型参数被crossinline关键字修饰

但是这里仍然可以用return@label的形式来显示指定return结束的代码作用域:
 

inline fun fun1(crossinline doSomething1: () -> Unit){
    Log.i("tag", "1")
    Runnable {
        doSomething1()
    }
}
 
//调用
fun mainFun() {
    fun1 {
        Log.i("tag", "2")
        return@fun1//编译正常
    }
    Log.i("tag", "3")
}


————————————————
版权声明:本文为CSDN博主「我们间的空白格」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37159335/article/details/123658961

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值