前言
说起内联函数,熟悉Kotlin的Android开发肯定使用过,比如我们常见的apply函数:
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
我们也大概知道这个inlie的作用是啥,不过一般都没有仔细思考过为什么,以及它还有2个好兄弟oninline和crossinline的作用,这篇文章我就准备从最简单的地方,挨个举例来说清楚这些。
正文
首先就是必须了解Kotlin的lambda以及函数类型,可以看我之前的文章:
这里有说了一个重要概念,就是Kotlin的高阶函数其实就是实现FunctionN的一个实例,还有就是Kotlin的lambda作为实参传递给参数时,也会创建匿名内部类以及调用invoke方法,我们这里来逐步深入。
Android Studio反编译Kotlin代码
这个是AS的很好用的一个功能,当我们写了一个Kotlin文件,但是我们想看它编译成Java是什么样子的,可以直接使用AS来进行。
比如下面代码:
如下显示Kotlin字节码:
然后点击这个反编译按钮:
然后便生成了对应的Java文件:
比如这里我们就可以看出Kotlin代码中的lambda这里创建了一个OnClickListener的实例对象,所以当不熟悉的Kotlin代码,把它反编译成对应的Java代码将会好容易理解很多。
lambda作为实参传入Kotlin高阶函数
这里还是复习一下Kotlin高阶函数中把lambda作为实参会发生什么,比如下面代码:
//在Kt文件中定义一个高阶函数
fun lambdaFun(action: (() -> Unit)){
Log.i("zyh", "testLambdaFun: 调用前")
action()
Log.i("zyh", "testLambdaFun: 调用后")
}
调用上面高阶函数:
fun testHello1(){
lambdaFun {
Log.i("zyh", "testLambdaFun: 正在调用")
}
}
给转换成Java代码:
public final class TestFun {
public final void testHello1() {
//把lambda转换成了Function0的实例
TestInlieKt.lambdaFun((Function0)null.INSTANCE);
}
}
会发现这里没啥问题,把lambda转换成匿名内部类实现合情合理,但是当有很多的地方都这样写:
//调用多次lambda
fun testHello1(){
for (i in 0 .. 10){
lambdaFun {
Log.i("zyh", "testLambdaFun: 正在调用")
}
}
}
反编译结果:
public final class TestFun {
public final void testHello1() {
int var1 = 0;
//创建了多个匿名内部类实例
for(byte var2 = 10; var1 <= var2; ++var1) {
TestInlieKt.lambdaFun((Function0)null.INSTANCE);
}
}
}
这里就会发现有问题了,当匿名内部类过多时,会导致内存增加,这里也就是inline关键字出现的原因。
inline关键字
inline关键字可以修饰函数,然后函数称之为内联函数,当函数是内联函数时,函数内部的函数体以及函数类型参数都会被“内联”到调用地方。
这里比较难理解,要理解2点,一个是函数内部的函数体的内联,一个传递进来的lambda表达式的内联,这个十分重要,在后面细说。
为了理解,我们还是举个简单例子:
//定义一个非高阶函数
fun normalFun(){
Log.i("zyh", "testLambdaFun: 调用前")
Log.i("zyh", "testLambdaFun: 调用后")
}
调用地方,然后进行反编译:
fun testHello(){
normalFun()
}
//反编译
public final class TestFun {
public final void testHello() {
TestInlieKt.normalFun();
}
}
这里就是正常调用,当把normalFun定义成inline函数:
//普通函数添加inline
inline fun normalFun(){
Log.i("zyh", "testLambdaFun: 调用前")
Log.i("zyh", "testLambdaFun: 调用后")
}
然后进行调用,然后反编译:
//反编译代码
public final class TestFun {
public final void testHello() {
int $i$f$normalFun = false;
Log.i("zyh", "testLambdaFun: 调用前");
Log.i("zyh", "testLambdaFun: 调用后");
}
}
看到这里是不是有一种豁然开朗的感觉,这里直接把normalFun函数内的逻辑直接复制到调用地方,很nice。
注意这里的操作是由Kotlin编译器干的事,所以我们可以不用探讨。
假如这里只有这个对普通函数的效果,那未免没有什么意思,顶多也就让调用站少了一层,但是让编译器干了这么多事,肯定不划算,真正的亮点在于高阶函数。
inline修饰高阶函数
inline不仅可以“铺平内联”函数内的代码,还可以“铺平内联”函数类型的参数,这才是关键,比如下面代码:
//高阶函数添加inline
inline fun lambdaFun(action: (() -> Unit)){
Log.i("zyh", "testLambdaFun: 调用前")
action()
Log.i("zyh", "testLambdaFun: 调用后")
}
然后进行调用,进行反编译:
//反编译代码
public final class TestFun {
public final void testHello() {
int $i$f$lambdaFun = false;
Log.i("zyh", "testLambdaFun: 调用前");
int var2 = false;
Log.i("zyh", "testLambdaFun: 调用中");
Log.i("zyh", "testLambdaFun: 调用后");
}
}
会惊喜地发现,这里反编译的代码没有匿名内部类的影子,这也就达到了我们的目的,即使这里调用多个lambda,假如被调用的函数被声明为了inline,那也不会创建出多个无用的类,可以大大减小内存使用。
总结
其实内联函数很关键,它解决了使用lambda方便的同时创建过多的匿名内部类的弊端,这里我更喜欢把被调用的inline函数的变化称为复制铺平,也就是把函数体的代码和函数类型参数给赋值铺平到调用地方,当然这种叫法只是个人理解。
既然赋值铺平很好用,那是不是所有inline函数的函数类型参数都可以复制铺平呢 我们下篇文章再说,也就是noinlien和crossinline的使用。