今天介绍下 Kotlin
中 非常重要的 内联函数,小伙伴系紧鞋带准备发车
普通函数调用
下面测试整数相加的情况
fun calculate() {
println(add(a = 1, b = 3))
}
fun add(a: Int, b: Int) = a + b
反编译查看下 java
代码(Android Studio或idea下可以使用 kotlin
插件直接查看,路径是 Tools → Kotlin → Show Kotlin Bytecode → DECOMPILE
)
public final void calculate() {
int var1 = this.add(1, 3);
System.out.println
}
final int add(int a, int b) {
return a + b;
}
可以看到这是一个正常的函数调用,在calculate
函数内部调用了add
inline修饰的普通函数
我们再来看下将add添加了inline
的效果
fun calculate() {
println(add(a = 1, b = 3))
}
inline fun add(a: Int, b: Int) = a + b
反编译查看下 java
代码
public final void calculate() {
byte a$iv = 1;
int b$iv = 3;
int $i$f$add = false;
int var1 = a$iv + b$iv;
System.out.println(var1);
}
final int add(int a, int b) {
int $i$f$add = 0;
return a + b;
}
可以看到calculate
调用了inline
函数add
,编译时期将add
函数方法体拷贝到了调用的地方,意味着方法的调用栈少了一层
add
函数虽然可以是可以添加inline
,不过编译器却给出了警告
大概意思就是inline
函数在此并不会提高性能,inline
更适合在函数参数为函数类型的函数中使用(高阶函数)
普通高阶函数
我们来看下不加inline
的lambda
参数的情况
fun calculate() {
add(a = 1, b = 3) {
println("a + b = $it")
}
}
fun add(a: Int, b: Int, result: (Int) -> Unit): Int {
val sum = a + b
result.invoke(sum)
return sum
}
反编译查看下 java
代码
public final void calculate() {
this.add(1, 3, (Function1)null.INSTANCE);
}
public final int add(int a, int b, Function1 result) {
int sum = a + b;
result.invoke(sum);
return sum;
}
是不是发现很奇怪,我们的lambda
竟然转换成了(Function1)null.INSTANCE
,这是个啥东西?
其实(Function1)null.INSTANCE
,是由于反编译器工具在找不到等效的 Java 类时的显示的结果。
这个时候我们就需要使用到我们的反编译工具 jadx
了,这里附带 jadx 的地址,有需要学习的小伙伴可自行查阅
jadx
下的代码是这样的
public final void calculate() {
add(1, 3, calculate$1.INSTANCE);
}
public final int add(int a, int b, Function1 super Integer, Unit> result) {
Intrinsics.checkNotNullParameter(result, "result");
int sum = a + b;
result.mo2833invoke(Integer.valueOf(sum));
return sum;
}
final class calculate$1 extends Lambda implements Function1 {
public static final calculate$1 INSTANCE = new calculate$1();
calculate$1() {
super(1);
}
@Override // kotlin.jvm.functions.Function1
/* renamed from: invoke */
public /* bridge */ /* synthetic */ Unit mo2922invoke(Integer num) {
invoke(num.intValue());
return Unit.INSTANCE;
}
public final void invoke(int it) {
System.out.println((Object) ("a + b = " + it));
}
}
可以看到 lambda
表达式转换成了一个 Function1
对象,它是 Kotlin
函数的一部分,那为什么是 Function1
呢,实际上是因为 lambda
中传递了一个参数,如果没传递参数则是 Function0
,以此类推。这个 Function1
对象的创建无疑是会消耗内存的,假如我们的代码中存在很多的高阶函数(参数类型是函数或者返回值类型是函数,在代码编译之后那么是不是会创建很多的 Function
对象呢?这个内存的消耗是不可估计的,所以 Kotlin
官方为了优化这个点,出现了 inline
内联函数
inline修饰的高阶函数
我们还是继续使用上面的例子来看下 inline
内联函数是如何提高性能的
fun calculate() {
add(a = 1, b = 3) {
println("a + b = $it")
}
}
inline fun add(a: Int, b: Int, result: (Int) -> Unit): Int {
val sum = a + b
result.invoke(sum)
return sum
}
继续反编译看下 java
代码
public final void calculate() {
byte a$iv = 1;
int b$iv = 3;
int $i$f$add = false;
int sum$iv = a$iv + b$iv;
int var7 = false;
String var8 = "a + b = " + sum$iv;
System.out.println(var8);
}
public final int add(int a, int b, @NotNull Function1 result) {
int $i$f$add = 0;
Intrinsics.checkNotNullParameter(result, "result");
int sum = a + b;
result.invoke(sum);
return sum;
}
可以看到相比于不加 inline
,方法的调用栈少了一层,并且不会生成额外的对象,这对内存还说是一个很棒的优化。一般情况下我们在高频调用的高阶函数下使用inline
,减少内存的消耗。
noinline
noinline
刚好跟 inline
相反, 它是让 高阶函数中函数类型的参数 不参与内联,先来看下为什么会有 noinline
函数类型的参数不止可以当做函数去调用,还可以当做对象去使用,例如我们把它当做函数返回值
我们还是以上面为示例修改下代码
fun calculate() {
add(a = 1, b = 3,
addPrev = {
println("addPrev!")
}, result = {
println("a + b = $it")
}, addPost = {
println("addPost!")
}
)
}
inline fun add(a: Int, b: Int, addPrev: () -> Unit, result: (Int) -> Unit, addPost: () -> Unit): () -> Unit {
addPrev.invoke()
val sum = a + b
result.invoke(sum)
return addPost
}
上面的代码编译是不会通过的,并且编译器给出了错误提示
我们知道在 inline
内联函数中函数类型的参数是不会创建函数对象的,它仅仅是作为一个函数体存在而不是一个函数对象,所以无法当成一个对象进行返回
如果我们还是需要将函数类型的参数作为对象去使用,编译器也给出了解决方案,给函数类型的参数加上 noinline
即可
fun calculate() {
add(a = 1, b = 3,
addPrev = {
println("addPrev!")
}, result = {
println("a + b = $it")
}, addPost = {
println("addPost!")
}
)
}
inline fun add(
a: Int,
b: Int,
addPrev: () -> Unit,
result: (Int) -> Unit,
noinline addPost: () -> Unit
): () -> Unit {
addPrev.invoke()
val sum = a + b
result.invoke(sum)
return addPost
}
crossinline
crossinline
是局部加强内联的意思
我们来看下下面这个场景,在 lambda
内部直接return
fun main() {
linpopopo {
println("linpopopo function")
return
}
println("main function")
}
inline fun linpopopo(action: () -> Unit) {
action.invoke()
}
这个return会结束那个函数?linpopopo?main?
按常理来说,这个return会结束 linpopopo函数的执行,后面的 println("main function")
会被执行,不过这里是 linpopopo
内联函数,它在编译的时候会将函数体移到调用的地方,我们来看下编译之后的代码就清楚了
public final void main() {
int $i$f$linpopopo = false;
int var3 = false;
String var4 = "linpopopo function";
System.out.println(var4);
}
public final void linpopopo(@NotNull Function0 action) {
int $i$f$linpopopo = 0;
Intrinsics.checkNotNullParameter(action, "action");
action.invoke();
}
看到了吧,这里 return 结束的是 main函数,即最外层的函数。甚至在编译的时候会将 println("main function")
舍弃掉不参与编译,因为它总是不会执行,参与编译毫无意义
那我们在函数类型参数的 lambda
表达式中的return的结果就要看该函数是不是内联函数了,这就让我们敲代码极其的不便利了,每个函数我们都进去看下是否是内联函数,这对我们的时间很大的消耗
后来 Kotlin
官方制定了一条新规则,lambda表达式中不允许直接使用return,除非这个 Lambda 是内联函数的参数,并且结束的是最外层的函数
解决上面的问题我们也可以使用return@label的方式结束代码作用域,例如直接return隐式标签 linpopopo
fun main() {
linpopopo {
println("linpopopo function")
return@linpopopo
}
println("main function")
}
inline fun linpopopo(action: () -> Unit) {
action.invoke()
}
反编译再看下代码
public final void main() {
int $i$f$linpopopo = false;
int var3 = false;
String var4 = "linpopopo function";
System.out.println(var4);
String var1 = "main function";
System.out.println(var1);
}
public final void linpopopo(@NotNull Function0 action) {
int $i$f$linpopopo = 0;
Intrinsics.checkNotNullParameter(action, "action");
action.invoke();
}
干得漂亮,非常符合我们的预期,老板都夸你解决问题的方式多,加鸡腿加鸡腿
再来看下下面这个场景,有的时候我们需要在主线程上面去执行 lambda
表达式,这里主线程是使用协程进行切换
fun main() {
linpopopo {
println("linpopopo function")
return
}
println("main function")
}
inline fun linpopopo(action: () -> Unit) {
MainScope().launch {
action.invoke()
}
}
这样会引起一个问题,linpopopo 函数和 main函数就属于间接调用关系,导致 lambda
表达式里的 return 无法结束 main 函数。那么它在这里结束的是谁?其实压根不会结束谁,因为上面这段代码根本不会编译通过,编译器也给出了错误提示
千呼万唤始出来,编译器让我们使用 crossinline
去修饰函数类型的参数,这样间接调用关系才会成立
inline fun linpopopo(crossinline action: () -> Unit) {
MainScope().launch {
action.invoke()
}
}
我们又回到了原始的问题,加了 crossinline
这里的 lambda
表达式里的 return到底结束了谁?是 main 函数 还是协程 launch作用域呢?
对于这种歧义的问题,Kotlin
官方又增加了一条新的规定,内联函数中被 crossinline
修饰的函数类型的参数不允许return
所以上面的 return
也不会编译成功,当然这里还是可以使用return@label的方式结束代码作用域的
fun main() {
linpopopo {
println("linpopopo function")
return@linpopopo
}
println("main function")
}
inline fun linpopopo(crossinline action: () -> Unit) {
MainScope().launch {
action.invoke()
}
}
总结
-
inline
: 编译时将函数体拷贝到调用的地方,减少函数类型对象的创建 -
noinline
: 局部关掉内联,解决不能把函数类型的参数当做对象来使用的问题 -
crossinline
: 局部加强内联,让内联函数中函数类型的参数可以间接被调用,并且crossinline
修饰的函数类型的参数不允许return
致谢
文中部分观点参考了扔物线大佬的作品,小伙伴可自行查阅哦,扔物线大佬文章很生动哈哈哈
原文地址:Kotlin源码里成吨的noinline和crossinline是干嘛的?看完视频你转头也写了一吨