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 {
        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版权协议,转载请附上原文出处链接及本声明。

原文链接:

每日一面——inline关键字和include头文件

写前声明:参考链接 C++面经、面试宝典 等

✊✊✊每日一面——inline关键字和include头文件

一、说说inline的作用及使用方法 二、内联函数一定会被编译器在调用点展开吗?

不一定,inline只是对编译器的建议而非命令。编译器也可以选择忽视inline。如果当定义的inline函数包含复杂递归、代码量大,编译器一般不会将其展开,仍然会选择函数调用

三、普通函数一定是被编译器调用吗?

这也是不一定的,即使对于普通函数,编译器也可以对此进行优化,将其在“函数调用点”上展开

四、既然内联函数在编译阶段就已经在调用点被展开,那么在程序运行期间,对应的内存中包不包括内联函数的定义?

肯定包括,如上知,编译器可以选择调用内联函数,而非直接展开它。因此,在内存中仍然需要一份内联函数的定义,以供调用。而且,一致性是所有语言都应该遵守的准则,普通函数有可以有指向它的函数指针,那么内联函数也可以有,因此内存中需要。。。,使得函数指针可以存在

五、inline函数的工作原理 六、宏定义(define)和内联函数(inline)的区别 七、为什么不把所以的函数都写成内联函数?

内联函数以代码复杂为代价,它以省去函数调用老提高执行效率。一方面,如果内联函数体代码执行时间比函数调用开销大,则没有太大的意义;另一方面,每一次内联函数的调用都需要复制代码,消耗更多的内存空间,因此以下情况不宜使用内联函数:

八、说说include头文件的顺序以及双引号""和尖括号的区别 区别: 查找路径:

本站内容来自用户投稿,如果侵犯了您的权利,请与我们联系删除。联系邮箱:835971066@qq.com

本文链接:http://news.xiuzhanwang.com/post/2127.html

发表评论

评论列表

还没有评论,快来说点什么吧~

友情链接: