内联函数/inline
简单来说,就是把函数的代码直接复制到调用处,而不是通过压栈的方式直接调用函数。
inline fun testInline(s:String):String{
println("内联函数")
if(s.equals("A")){
println("是A")
}else{
println("不是A")
}
return "Hello,$s"
}
fun testNoInline(s:String):String{
println("非内联函数")
if(s.equals("A")){
println("是A")
}else{
println("不是A")
}
return "Hello,$s"
}
fun main() {
testNoInline("World")
testInline("World")
}
*********** ********** ****************
//编译后的代码是这样的
public final class KotlinShowKt {
@NotNull
public static final String testInline(@NotNull String s) {
Intrinsics.checkNotNullParameter(s, "s");
System.out.println((Object) "内联函数");
if (s.equals("A")) {
System.out.println((Object) "是A");
} else {
System.out.println((Object) "不是A");
}
return "Hello," + s;
}
@NotNull
public static final String testNoInline(@NotNull String s) {
Intrinsics.checkNotNullParameter(s, "s");
System.out.println((Object) "非内联函数");
if (s.equals("A")) {
System.out.println((Object) "是A");
} else {
System.out.println((Object) "不是A");
}
return "Hello," + s;
}
public static final void main() {
testNoInline("World");
System.out.println((Object) "内联函数");
if ("World".equals("A")) {
System.out.println((Object) "是A");
} else {
System.out.println((Object) "不是A");
}
String str = "Hello,World";
}
}
可见,对于非inline函数,在调用的时候是直接调用函数,而调用inline函数则是直接把函数的实现代码复制到调用处。这样做有什么好处呢?
(1)直接调用方法会创建栈帧、压栈、出栈等开销,而代码复制过去,就没有这些开销,性能可以更好一点。尤其对于一些高阶函数,它们有些参数是lambda形式的,如果传给它们的不是inline函数,其执行机制将按照Java的套路来------创建匿名对象,如果是inline函数,则可避免这些,性能开销会更小。
inline fun repeatInline(times: Int, action: (Int) -> Unit) {
for (i in 0 until times) {
action(i)
}
}
fun repeatNoInline(times: Int, action: (Int) -> Unit) {
for (i in 0 until times) {
action(i)
}
}
fun main() {
repeatInline(3) { i ->
println("i = $i")
}
repeatNoInline(3) { i ->
println("i = $i")
}
}
*************
//编译后得到的Java代码
public static final void repeatInline(int times, @NotNull Function1<? super Integer, Unit> function1) {
Intrinsics.checkNotNullParameter(function1, "action");
for (int i = 0; i < times; i++) {
function1.invoke(Integer.valueOf(i));
}
}
public static final void repeatNoInline(int times, @NotNull Function1<? super Integer, Unit> function1) {
Intrinsics.checkNotNullParameter(function1, "action");
for (int i = 0; i < times; i++) {
function1.invoke(Integer.valueOf(i));
}
}
public static final void main() {
for (int i$iv = 0; i$iv < 3; i$iv++) {
int i = i$iv;
System.out.println((Object) ("i = " + i));
}
repeatNoInline(3, (v0) -> {
return main$lambda$1(v0);
});
}
private static final Unit main$lambda$1(int i) {
System.out.println((Object) ("i = " + i));
return Unit.INSTANCE;
}
(2)允许在 lambda 中使用非局部返回(return 从外层函数返回)。
在 Kotlin 中,非局部返回 指的是:从一个 lambda 表达式内部使用 return 关键字,直接从包含这个 lambda 的外层函数返回,而不是仅仅从 lambda 本身退出。这是一种强大的控制流能力,让 lambda 可以像语言内置的关键字(如 if、for、while)一样影响外层函数的执行流程。这一点很好理解,由于内联函数是把代码复制过去,那么内联函数里的return其实也就是相当于在调用内联函数的地方直接return了。
深度思考
有没有注意到,内联函数和非内联函数在转成Java代码后是一样的,比如上面的repeatInline和repeatNoInline。这是怎么回事?
(1)向后兼容。如果Kotlin最初发布时一个函数是普通函数,后来感觉该函数性能敏感,需要改成内联函数,此时如果编译器不为其生成如上的普通Java方法,那么旧的代码想要调用的方法就没有了。也就是,该函数原来非内联函数,编译后生成了普通的Java方法,如果改成了内联函数后就不再为其生成普通的Java方法了,那旧代码就无法运行了。
(2)Java互操作性。Java是没有内联这种机制的,不会把代码复制过去,所以为了能让Java调用Kotlin的内联函数,还是要为Java生成一个普通的非内联函数。
(3)一些场景无法使用内联。比如反射调用方法,将函数作为值传递这种需要函数引用的场合等。所以也要为这些场合留下备用方案。
所以,非内联函数,还是老样子,编译成普通Java代码;内联函数,也要编译成一个普通Java方法;真正让内联函数发挥作用的是Kotlin编译器在编译Kotlin代码时做了代码复制的操作。
理论上非内联函数的lambda参数,不应该是生成一个匿名对象吗?怎么上面的repeatNoInline方法并没有生成匿名对象,而是生成了一个方法mainlambda1(int i)?
Kotlin 编译器在不同 JVM 目标版本下的优化策略不同。
在JVM 1.8版本之前,确实会生成如下所示的匿名对象,编译后代码应该是这样的
... ...
repeatNoInline(3, new Function1<Integer, Unit>() {
public Unit invoke(Integer i) {
System.out.println("i = " + i);
return Unit.INSTANCE;
}
});
每次调用repeaNoInline都会生成一个匿名对象,开销比较大,而且还会生成对应类的.class文件。什么意思呢?
fun main() {
repeatNoInline(3) { println("first") }
repeatNoInline(3) { println("second") }
}
如上代码,将会产生TestKt.class、TestKtmain1.class(对应第一个 lambda)、TestKtmain2.class(对应第二个 lambda)。所以开销还是比较大的。
JVM 1.8+开始,采用更高效的 Java 8 lambda 表示 + invokedynamic方案。当Kotlin编译器遇到lambda时,编译器生成的是 invokedynamic 指令,而不是 new 匿名内部类的字节码。而上述代码是我用jadx-gui查看的编译出的.class文件,反编译器在遇到**invokedynamic** 时,反编译器将 invokedynamic 翻译成了 Java 8 的 lambda 语法。实际上,还是产生了匿名对象,只是相比1.8之前,现在的方案更加高效了。invokedynamic,顾名思义,动态调用,简单来说,就是运行时根据场景做调整,也就有了优化的空间。对于无捕获的 lambda(不引用外部变量),通常使用LambdaMetafactory 生成一个单例对象(静态字段),所有调用点共享同一个实例。 即使 lambda 表达式出现在循环中,也不会反复创建新对象,只会复用那个单例。对于有捕获的 lambda(例如引用了外部局部变量 val x = 1; { println(it + x) }),每次执行到该 lambda 表达式时,仍然会创建一个新对象(因为需要保存不同的捕获值),但实现方式比匿名内部类更高效:类在运行时由 LambdaMetafactory 动态生成,不产生磁盘文件,且只生成一次 (同一个 lambda 位置,相同的捕获类型)。类加载通过内部机制完成,比从磁盘加载 .class 快得多,而且缓存在内存里。
总结:不管是1.8之前还是1.8之后,反正Java对于lambda的支持都少不了匿名对象,区别只是对lambda的支持越来越高效。
noinline
如果不想让某个函数类型的形参内联,就可以用noinline修饰
inline fun test(a:Int,b:Int,add:(Int,Int)->Int,noinline mul:(Int,Int)->Int){
val x = add(a,b)
val y = mul(a,b)
println(x)
println(y)
}
fun main() {
test(4,5,{x,y->x+y},{x,y->x*y})
}
//编译后
public final class KotlinShowKt {
public static final void test(int a, int b, @NotNull Function2<? super Integer, ? super Integer, Integer> function2, @NotNull Function2<? super Integer, ? super Integer, Integer> function22) {
Intrinsics.checkNotNullParameter(function2, "add");
Intrinsics.checkNotNullParameter(function22, "mul");
int x = ((Number) function2.invoke(Integer.valueOf(a), Integer.valueOf(b))).intValue();
int y = ((Number) function22.invoke(Integer.valueOf(a), Integer.valueOf(b))).intValue();
System.out.println(x);
System.out.println(y);
}
public static final void main() {
Function2 mul$iv = (v0, v1) -> {
return main$lambda$1(v0, v1);
};
int x$iv = 4 + 5;
int y$iv = ((Number) mul$iv.invoke(4, 5)).intValue();
System.out.println(x$iv);
System.out.println(y$iv);
}
private static final int main$lambda$1(int x, int y) {
return x * y;
}
}
这样,内联的函数形参就会直接代码复制形式执行,非内联的函数形参就以对象的方法的方式执行。
内联函数适合什么场景下使用?
(1)性能敏感的高频调用高阶函数。内联函数可避免匿名对象的创建,效率更高。
(2)需要非局部返回的控制流 DSL。
(3)需要泛型实化(reified)的场景。
(4)极小的工具函数(单行或几行代码)
不适合的场景
(1)函数体很大。每个调用点都会复制大片代码,导致字节码膨胀,甚至降低 CPU 缓存命中率。
(2)递归函数:编译器禁止内联递归函数。
(3)跨模块公开 API:内联函数的修改会强制所有调用方重新编译,否则二进制不兼容。