本篇是记录学习《Kotlin实战》一书中关于内联的概念和知识点
1.内联函数介绍
在Kotlin中如果使用inline修饰符标记的一个函数,被称为内联函数。在解释内联函数的作用之前,我们先来看下如果没有内联修饰符标记函数,在使用lambda带来的性能开销。举个接受函数类型的例子
//callAction 接受一个函数类型(lambda)
private fun callAction(action: () -> Unit) {
println("call Action before")
action()
println("call Action after")
}
fun main(args: Array<String>) {
callAction {
println("call action")
}
}
这两个函数会被编译为
public final void main(@NotNull String[] args) {
callAction((Function0)null.INSTANCE);
}
private final void callAction(Function0 action) {
String var2 = "call Action before";
boolean var3 = false;
System.out.println(var2);
action.invoke();
var2 = "call Action after";
var3 = false;
System.out.println(var2);
}
由此可见当调用callAction(action: () -> Unit) 时,传递的lambda会被Function0所代替,而Function0是一个被定义为如下的接口
public interface Function0<out R> : Function<R> {
public operator fun invoke(): R
}
也就是说在调用callAction时,编译器会额外生成一个Function0的实例传递给callAction,内部会调用 Function0 的 invoke() 方法。到目前为止,我们知道使用lambda会带来额外的性能开销。那有什么办法可以解决此问题呢?答案当然有,通过 内联函数消除lambda带来的运行时开销
2 内联函数的作用
在函数被使用的时候编译器并不会生成函数调用的代码,而是使用函数实现的真实代码替换每一次的函数调用。还是拿 callAction(action: () -> Unit) 方法举例,当给该函数添加inline修饰符后,编译后的调用代码如下
public final void main(@NotNull String[] {
...省略无关紧要的代码
System.out.println("call Action before");
System.out.println("call action");
System.out.println("call Action after");
}
代码结构如此清晰。 总结下:
- 被inline修饰的函数叫内联函数
- 内联函数会在被调用的位置内联。内联函数的代码会被拷贝到使用它的位置,并把lambda替换到其中
3 内联函数的限制
鉴于内联的运作方式,不是所有使用lambda的函数都可以被内联。当函数被内联的时候,作为参数的lambda表达式的函数体会被直接替换到最终生成的代码中。这将限制函数体中的对应(lambda)参数的使用。如果(lambda)参数被调用,这样的代码能被容易地内联。但如果(lambda)参数在某个地方被保存起来,以便后面可以继续使用,lambda表达式的代码将不能被内联,因为必须要有一个包含这些代码的对象存在。
例如下面的例子,action被隐式保存在runAction函数中,然后再传递到callAction中。此时lambda表达式代码将不能被内联。
fun main(args: Array<String>) {
runAction {
println("run Action")
}
}
private fun runAction(action: () -> Unit) {
println("do something")
callAction(action)
println("do something")
}
private inline fun callAction(action: () -> Unit) {
println("call Action before")
action()
println("call Action after")
}
编译后的代码如下略有删减只保留重要部分
public final void main(@NotNull String[] args) {
runAction((Function0)null.INSTANCE);
}
private final void runAction(Function0 action) {
System.out.println("do something");
System.out.println("call Action before");
action.invoke();
System.out.println("call Action after");
System.out.println("do something");
}
如果想让callAction内联只能将runAction也改为内联函数。一般来说,参数如果被直接调用或者作为参数传递给另外一个inline函数,它是可以被内联的。(内联函数传递内联函数)
如果一个函数期望两个或更多lambda参数,可以选择只内联其中一些参数。这是有道理的,因为一个lambda可能会包含很多代码或者以不允许内联方式使用。接口这样的非内联lambda的参数,可以用noinline修饰符来标记它。
private inline fun callAction(action: () -> Unit, noinline doSomething: () -> Unit) {
}
5 内联函数另一妙用
无论使用Java还是Kotlin泛型时,在运行时都无法避免泛型擦除。例如如下例。
fun main(args: Array<String>) {
printSum(listOf<String>("a", "b", "c"))
}
fun printSum(c: Collection<*>) {
val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected")
println(intList.sum())
}
在调用printSum()时传递的是List泛型为String类型 运行时由于泛型擦除那么List泛型String类型和List 泛型Int类型变为List。那么第一行实际是 List as? List 成立。在调用sum方法时由于实际参数是String,运行时会得到一个ClassCastException。
Kotlin泛型在运行时会被擦除,这意味着如果你有一个泛型类的实例,你无法弄清楚在这个实例创建时用的究竟是哪些类型实参。泛型函数的类型实参也是这样。在调用泛型函数的时候,在函数体中你不能决定调用它用的类型实参。例如
fun <T> isA(value :Any) =value is T
这行代码编译器会报错:==Cannot check for instance of erased type:T== (无法检查已擦除类型的实例:T )
通常情况下都是这样,只有一种例外可以避免这种限制:内联函数。内联的类型形参能够被实化,意味着你可以在运行时引用实际的类型实参。
inline fun <reified T> isA(value :Any) =value is T
接下来看看使用实化类型参数来代替传递作为java.lang.Class的activity类的妙用。
//类型参数标记为"reified"
inline fun <reified T : Activity> Context.startActivity() {
//把T:class当做类型参数的类访问
val intent = Intent(this, T::class.java)
startActivity(intent)
}
<!--MainActivity调用-->
startActivity<MainActivity>()
6 决定何时将函数声明成内联
在使用inline关键字的时候,你还是应该注意代码长度。如果你要内联的函数很大,将它的字节码拷贝到每个调用点将会极大地增加字节码的长度。在这种情况下,你应该将那些与lambda参数无关的代码抽取到一个独立的非内联函数中,或者将不依赖实化类型参数的代码抽取到单独的非内联函数中,你可以自己去验证下,在Kotlin标准库中的内联函数总是很小。