Kotlin中内联函数的理解
kotlin中使用高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭包。即那些在函数体内会访问到的变量。内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销。
调用一个方法是一个压栈和出栈的过程,调用方法时将栈针压入方法栈,然后执行方法体,方法结束时将栈针移出栈,这个压栈和出栈的过程会耗费资源,这个过程中传递形参也会耗费资源。
来看一个官方的例子:
1 | fun <T> lock(l: Lock, body: () -> T): T { |
调用这个方法:
1 | lock(l, {"do something!"})//l是一个Lock对象 |
对于编译器来说,调用lock方法就要将参数l和lambda表达式{“do something!”}进行传递,还要将lock方法进行压栈出栈处理,这个过程就会耗费资源。如果只要函数体类似这样:
1 | l.lock() |
这样做的效果和调用lock方法是一样的,而且不需要压栈出栈了,但是如果代码中频繁调用lock方法,必然要复制大量重复代码,那么有没有一种机制,又能少些重复代码(变成一个可供调用的方法)又不会在调用过程中频繁压栈出栈影响性能呢。有的,这就是kotlin的内联函数inline所拥有的能力。
inline
使用inline声明的函数,会在编译时将会拷贝到调用的地方。
inline function
定义一个sum函数计算两个数的和
1 | fun main(args: Array<String>) { |
反编译为Java代码看看
1 | public static final void main(@NotNull String[] args) { |
正常的样子,在该调用的地方调用函数。
然后为sum函数添加inline声明:
1 | public static final void main( String[] args) { |
再反编译为Java代码:
1 | public static final void main(@NotNull String[] args) { |
可以看到sum函数的实现代码被直接拷贝到了调用的地方。上面的例子其实并没有体现inline的优势,因为拷贝代码和在调用的地方调用方法没有本质区别,但是如果你的函数中有lambda形参数,或者是参数为函数的时候,inline的优势才会体现(因为不会新建函数对象,可以减少内存损耗)。
inline function with lambda parameters
再来看一个例子:
1 | fun sum(a: Int, b: Int, lambda: (result: Int) -> Unit): Int { //sum方法中有一个函数参数 |
反编译为java:
1 | public static final int sum(int a, int b, @NotNull Function1 lambda) { |
(Function1)null.INSTANCE,是由于反编译器工具在找不到等效的 Java 类时的显示的结果。
我们传递的那个lambda被转换为了Function1类型,它是Kotlin函数的一部分,它以1结尾是因为我们在lambda函数中传递了一个参数。
再来看一个代码:
1 | fun main(args: Array<String>) { |
我们在循环中调用sum函数,每次都传递一个lambda函数打印结果,反编译为java:
1 | for(byte var2 = 10; var1 <= var2; ++var1) { |
可见在每次循环里面都会创建一个Function1的实例对象,这里就是性能的优化点所在,如何优化呢?
- 在循环外部建立lambda对象
1 | val l: (r: Int) -> Unit = { println(it) } |
反编译为java
1 | Function1 l = (Function1)null.INSTANCE; |
只会创建一个Function对象,优化了原来在循环内部不停创建对象。
- 使用inline
1 | fun main(args: Array<String>) { |
反编译java
1 | public static final void main(@NotNull String[] args) { |
lambda函数对象在编译的时候被拷贝到了调用的地方,避免了创建Fuction对象
inline使用注意事项
- public inline函数不能访问私有属性
1 | class Demo(private val title: String) { |
- 注意程序控制流程
当使用inline的时候,如果传递给inline函数的lambda,有return语句,那么会导致闭包的调用者也返回。
看个例子:
1 | inline fun sum(a: Int, b: Int, lambda: (result: Int) -> Unit): Int { |
反编译java
1 | public static final void main(@NotNull String[] args) { |
反编译之后也能看到,lambda return之后的代码不会执行。(println(“Done”)没有执行)。怎么解决这个问题呢,不要使用return可以使用return@label语法,返回到lambda被调用的地方。
1 | fun main(args: Array<String>) { |
noinline
当一个inline函数中,有多个lambda函数做为参数的时候,可以在不想内联的lambda函数前使用noinline声明。不会被拷贝代码到被调用的地方。
看一个例子:
1 | inline fun sum(a: Int, b: Int, lambda: (result: Int) -> Unit, noinline lambda2: (result: Int) -> Unit): Int { |
反编译为java:
1 | public static final int sum(int a, int b, @NotNull Function1 lambda, @NotNull Function1 lambda2) { |
从反编译代码中可以看到使用inline声明的lambda形参函数被搬到了被调用的地方,而被noinline声明的函数则生成了Function对象。
crossinline
声明一个lambda函数不能有return语句(可以有return@label语句),这样可以避免使用inline时,lambda中的return影响流程导致某些语句没有执行。被crossinline声明的lambda函数如果有return语句会在编译时报错。
例子:
1 | inline fun sum(a: Int, b: Int, crossinline lambda: (result: Int) -> Unit): Int { |
总结
- 使用 inline,内联函数到调用的地方,能减少函数调用造成的额外开销,在循环中尤其有效。
- 使用 inline 能避免函数的 lambda 形参额外创建 Function 对象。
- 使用 noinline 可以拒绝形参 lambda 内联。
- 使用 crossinline 显示声明 inline 函数的形参 lambda 不能有 return 语句,避免lambda 中的 return 影响外部程序流程。