Kotlin基础五:inline noinline crossinline

38 阅读5分钟

前言

kotlin源码中常见inline noinline crossinline的使用。内联函数 禁用内联等在阅读kotlin源码和开发中是不可避免的。

局部返回

Kotlin中非内联的Lambda表达式不支持使用裸return的,在非内联的Lambda表达式内部用@标签限制的return进行局部返回。如果强行在Lambada表达式中使用裸return,编译器会报语法错误。

fun main(){
    normal{
        println("test")
        return@normal
    }
}
fun normal(block:()->Unit){
    block()
}

这种写法称返回的标签。一个高阶函数在调用时,函数类型参数初始化Lambda会拥有默认的隐式标签,通常按照它外部的函数名类命名。也可以自己定义标签名称

fun main(){
    // kotlin版本号
    val kotlinVersion = KotlinVersion.CURRENT
    println("version:$kotlinVersion")
    normal test@{
        println("test")
        return@test
    }
}
fun normal(block:()->Unit){
    block()
}

return 只是局部返回

fun test() {
    val kotlinVersion = KotlinVersion.CURRENT
    println("version:$kotlinVersion")
    println("test call")
    normal { 
        println("normal call")
        return@normal
        println("normal call end")
    }
    println("test call end")
}
fun normal (block:()->Unit){
    block()
}

inline

inline修饰一个函数,就是内联函数。不仅可以内联自己函数体内部代码,还可以内联函数体内部函数的代码(Lambda表达式中的代码)

  • 内联函数主要目的为了在编译时展开,以消除函数调用的额外开销。可以保持高效,同时允许函数编程。免除了java中的入方法栈与退栈。
fun test() {
    normal {
        println("normal call")
    }
}
inline fun normal (block:()->Unit){
    println("normal start")
    block()
    println("normal end")
}

Kotlin代码最终还是要编译成Java字节码。在AndroidStudio中Tools->Kotlin->Show Kotlin Bytecode,点击Decompile按钮。 Lambda表达式在java中是用匿名类实现的。每调一次高阶函数normal就会创建一个Function匿名类,内存上造成额外的开销。当用inline修饰normal函数时反编译成Java字节码后,相当于将normal中的代码copy到了test内部。

  1. 普通函数,用内联函数时没必要的,只减少了一次方法栈的调用,这种优化可以忽略。
  2. 带有函数类型参数的高阶函数,使用inline修饰的内联函数,节省Lambda表达式在调用的地方创建匿名类带来的内存开销。
  3. 内联函数,不仅内联自己内部的代码,还可以内联内部的内部中的代码(Lambda表达式中的代码)。在调用的地方仅仅只是代码替换,可以在Lambda表达式中,直接裸return完成最外层函数的返回。这种返回(位于lambda表达式中,但退出包含它的函数)称非局部返回
  4. 如果需要内联的函数代码逻辑过于复杂,调用该函数比较频繁,会导致调用该内联函数的地方出现代码臃肿的情况。

noinline

nolinline禁用内联

  • 在某些情况下,内联函数可能会导致代码生成问题,如:循环展开、栈溢出问题。使用noinline可以解决这些问题。当你标记为noinline时,编译器会尽量避免将其内联。需要对函数进行性能分析或调试非常有用。
inline fun normal(block1:()->Unit, block2:()->Unit){
    block1()
    simple(block2)// 编译不过,内联函数的函数类型参数只能传递给内联函数
}

fun simple(block:()->Unit){
    block()
}

定义两个高阶函数normal和simple。内联函数的函数类型参数在编译的时候没有具体的参数类型的,它只是进行代码替换。内联函数的函数类型参数只能传递内联函数。而noinline关键字就是解决这个问题:

inline fun normal(block1:()->Unit, noinline block2:()->Unit){
    block1()
    simple(block2)// 编译通过: normal函数的函数类型参数加上了noinline
}

fun simple(block:()->Unit){
    block()
}

高阶函数normal中block2参数已经取消了内联的资格。编译器不会报语法错误了。

crossinline

一些内联函数可能会将自身拥有的函数类型参数实例的调用放在来自另一个上下文作用域的Lambda表达式或匿名类中。 将UI代码放在主线程中去执行:

inline fun runInMainThread(block:()->Unit){
    if (Looper.myLooper() == Looper.getMainLooper()){
        println("main thread")
    }else{
        CoroutineScope(Dispatchers.Main).launch { 
            block()// 编译不通过block
        }
        println("no main thread")
    }
}

给View添加一个postDelayed()扩展函数

inline fun View.postDelayed(delayInMillis:Long,action:()->Unit):Runnable{
    val runnable = Runnable{action()}// 编译不通过action
    postDelayed(runnable,delayInMillis)
    return runnable
}

不在内联函数体内部直接调用函数类型参数的实例,将其放在一个拥有另一个上下文的Lambda表达式或匿名类中,称间接调用。间接调用一个函数类型参数的实例是不支持在该函数类型初始化的Lambda表达式中使用裸return。我们又在内联函数的Lambda表达式中使用了裸return。两者语法产生冲突,kotlin直接提示了语法错误。crossinline可以解决,告知编译器,这种间接调用函数类型的实例的内联函数,一定不好再调用该内联函数的Lambda表达式中使用裸return

    fun test() {
        runInMainThread { 
//            return// 已经保证用crossinline标记了,不能return
            return@runInMainThread // 只能使用return@
        }
    }

    inline fun runInMainThread(crossinline block:()->Unit){
        if (Looper.myLooper() == Looper.getMainLooper()){
            println("main thread")
        }else{
            CoroutineScope(Dispatchers.Main).launch {
                block()// 编译不通过block
            }
            println("no main thread")
        }
    }

同时向Kotlin编译器保证,不好在调用内联函数的时候,在该内联函数的Lambda中用裸return。如果再去内联函数的Lambda调用裸return,编译器会报错误。可以使用return@了。

总结

inline noinline crossinline 关键字在以往的开发中会经常用到,对于阅读源码和实际开发帮助很大。