kotlin 内联函数的原理与应用

794 阅读5分钟

高阶函数

一个函数可以将另一个函数当作参数。将其他函数用作参数的函数称为“高阶函数”。

当接口中只有一个函数时使用高阶函数可省略接口,比如项目中常见的接口回调:

接口方式:

private var clickListener :ClickListener ?=null

fun setClickListener(clickListener :ClickListener){
   this.clickListener=clickListener
}

interface ClickListener{
    fun click(url:String)
}    
 
View.setOnClickListener {
    clickListener?.click(bean?.url?:"")
}   
    
调用:
 goodsAdapter?.setClickListener(object :BottomGoodsAdapter.ClickListener {
         override fun click(url: String) {
         dismiss() 
         MyApplication.getBus().post("start_floatingView:$url")
        }
 })   

kotlin高阶函数方式实现:

lateinit var mListener: (String) -> Unit

fun setClickListener(listener: (String) -> Unit) {
        this.mListener = listener
}
    
View.setOnClickListener {
     mListener(bean?.url?:"")
 } 

调用:
goodsAdapter?.setClickListener {
      dismiss() 
      MyApplication.getBus().post("start_floatingView:$it")
 }

可以看出使用kotlin高阶函数后,一没有了接口定义,二是没有了匿名内部类,代码更简洁。

内联函数

我们的项目里常常会创建一些 Util 类,用于分类整理那些会在许多地方用到的小型函数 ,如果这类函数接收了另一个函数作为参数(即高阶函数),则可能会造成一些额外的对象分配,通过使用 inline关键字,您可以避免这种情况并提升应用性能。

1.函数调用——工作原理

internal object Utils {
    @JvmStatic
    fun calculate(a: Int, b: Int, cal: (Int, Int) -> Int): Int {
        return cal(a, b)
    }

    @JvmStatic
    fun main(args: Array<String>) {
        calculate(6, 3,{ a, b -> a+b })
        calculate(4, 5){ a, b ->
            println("$a * $b = ${a * b}")
            a * b
        }
    }
}

这样其实是看不出什么问题的,Kotlin 文件编译后会生成对应的 class 文件,所以我们将 class 文件反编译成 Java 文件后再看。如果使用Android Studio或者IntelliJ IDEA,可以按照如下方式查看 Kotlin 文件对应反编译后的 Java 文件:

打开目标 Kotlin 文件 查看 Kotlin 文件字节码:Tools –> Kotlin –> Show Kotlin ByteCode 在 kotlin 文件字节码页面中点击左上角的 decompile 按钮,就会生成对应的 Java 文件

可以看出,编译器创建了两个Lambda的实例,并进行了两次calculate函数调用。每个高阶函数都会造成函数对象的创建和内存的分配,从而带来额外的运行时开销。

2.内联函数——工作原理

为了提升我们应用的性能,我们可以通过使用 inline 关键字,来减少函数对象的创建:

internal object Utils {
    @JvmStatic
    inline fun calculate(a: Int, b: Int, cal: (Int, Int) -> Int): Int {
        return cal(a, b)
    }

    @JvmStatic
    fun main(args: Array<String>) {
        calculate(6, 3,{ a, b -> a+b })
        calculate(4, 5){ a, b ->
            println("$a * $b = ${a * b}")
            a * b
        }
    }
}

我们再看最终的 Java 文件:

由于使用了 inline 关键字,编译器会将内联函数的内容复制到调用处,从而避免了创建新的函数对象。 需要注意的是, 内联函数提高代码性能的同时也会导致代码量的增加,所以应避免内联函数过大。

3.应该在哪些地方使用 inline 标记?

(1) 如果您试图标记为内联函数的函数,并没有接收另一个函数作为参数,您将无法获得明显的性能提升,而且 IDE 甚至会建议您移除 inline 标记.

(2) 因为 inline 关键字可能会增加代码的生成量,所以一定要避免内联大型函数。举例来说,如果去查看 Kotlin 标准库中的内联函数,您会发现它们大部分都只有 1 - 3 行。

4.禁用内联(noinline)

如果您的函数有多个函数参数,但是您需要持有其中某个的引用时,您可以将对应的参数标记为 noinline。

internal object Utils {

    @JvmStatic
    inline fun calculate(a: Int, b: Int, title:()->Unit, cal: (Int, Int) -> Int): Int {
        title()
        return cal(a, b)
    }

    @JvmStatic
    fun main(args: Array<String>) {
        calculate(6, 3, { println("开始计算") },{ a, b -> a+b })
    }
}

internal object Utils {

    @JvmStatic
    inline fun calculate(a: Int, b: Int,noinline title:()->Unit, cal: (Int, Int) -> Int): Int {
        title()
        return cal(a, b)
    }

    @JvmStatic
    fun main(args: Array<String>) {
        calculate(6, 3, { println("开始计算") },{ a, b -> a+b })
    }
}

通过使用 noinline,编译器就只会为对应函数创建新的 Function 对象,其余的则依旧会被内联。

为了减少 lambda 表达式带来的额外内存分配,建议您使用 inline 关键字!只需注意,标记对象最好是接收一个 lambda 表达式作为参数的小型函数。如果您需要持有 (作为内联函数参数的) lambda 表达式的引用,或者想要将它作为参数传递给另一个函数,使用 noinline 关键字标记对应参数即可。

5.非局部返回

默认情况下,在高阶函数中,要显式的退出(返回)一个 Lambda 表达式,需要使用 return@标签的语法,不能使用裸return,但这样也不能使高阶函数和调用高阶函数的函数退出。

internal object Utils {
    fun getResult(num:Int,sum:(Int)->Unit){
        sum(num)
        println("我在sum计算结束之后")
    }
    
    @JvmStatic
    fun main(args: Array<String>) {
        getResult(10){
            it+10
            println("$it+10=${it+10}")
            return@getResult
        }
        println("我在调用getResult之后")
    }
}

打印结果为:

但如果把 Lambda 表达式作为参数传递给一个内联函数,就可以在 Lambda 表达式中正常的使用return语句了,并且会使该内联函数和调用该内联函数的函数退出(返回),这种操作就是非局部返回。

internal object Utils {
    @JvmStatic
    fun main(args: Array<String>) {
        getResult(10){
            it+10
            println("$it+10=${it+10}")
            return
        }
        println("我在调用getResult之后")
    }

    inline fun getResult(num:Int,sum:(Int)->Unit){
        sum(num)
        println("我在sum计算结束之后")
    }

}

打印结果为:

在使用了非局部返回后,Lambda 表达式中return的返回值受调用该内联函数的函数的返回值类型影响。

fun test(){
        getResult(10){
            it+10
            println("$it+10=${it+10}")
            return   //此处的return 返回值类型需跟test函数的返回值类型一致
        }
    }

6.禁用非局部返回(crossinline)

内联函数可以使 Lambda表达式实现非局部返回,但是,如果一个内联函数的函数类型参数被crossinline修饰,则对应传入的 Lambda表达式将不能非局部返回了,只能局部返回了。

internal object Utils {
    @JvmStatic
    fun main(args: Array<String>){
        getResult(10){
            it+10
            println("$it+10=${it+10}")
            return@getResult
        }
        println("我在调用getResult之后")
    }

    inline fun getResult(num:Int, crossinline sum: (Int) -> Unit){
        sum(num)
        println("我在sum计算结束之后")
    }

}

打印结果为:

7.具体化的类型参数(reified)

在java中如果是泛型,是不能直接使用泛型的类型的(可以通过反射做到),但是kotlin却是可以的。 在内联函数中支持具体化的参数类型,即用reified来修饰需要具体化的参数类型,这样我们用reified来修饰泛型的参数类型,以达到我们的目的。

inline fun <reified T : Activity> Activity.startActivity() {
     startActivity(Intent(this, T::class.java))
}

startActivity<MainActivity>()

再比如需要创建一个Fragment的实例,并且要传递参数

之前你可能会这样写:

class MyFragment : Fragment() {
    fun newInstance(param: Int): MyFragment {
        val fragment = MyFragment()
        val args = Bundle()
        args.putInt("key", param)
        fragment.arguments = args
        return fragment
    }
}

需要的Fragment都要写个这个,不够优雅而且太麻烦。 现在通过reified来优化:

internal object Utils {
    inline fun <reified F : Fragment> Context.newFragment(key:String, value:String): F {
        val bundle = Bundle()
        bundle.putString(key, value)
        return Fragment.instantiate(this, F::class.java.name, bundle) as F
    }
}

调用:
newFragment<MyFragment>("name","lisa")