内联函数产生原因和原理

2,287 阅读7分钟

为什么要内联函数

  • 因为: Kotlin为了书写简单,所以引入了lambda。

  • 但是: lambda会造成性能消耗。

  • 所以: 引入了内联函数来解决这个问题。

如何证明lambda书写简单

我们来实现个需求,diff一下有lambda和无lambda的代码便知。

需求: 实现一个函数回调,回调一个String给我。

Java版本(无lambda):

// 首先需要定义一个回调
public interface Action {
    void click(String fuck);
}

// 然后定义这个方法,参数就是回调的接口
public void func(Action action) {
    String str = "hello";
    action.click(str);
}

// 最后调用它
public static void main(String[] args) {
    // 这里需要创建一个匿名类
    func(new Action() {
        @Override
        public void click(String fuck) {
            System.out.println(fuck);
        }
    });
}

然后我们来看kotlin版:

// 直接定义方法,参数是个表达式函数
fun func(action: (String) -> Unit) {
    val str = "hello"
    action.invoke(str)
}

// 调用
fun main() {
    // 参数直接传入lambda就完事,it是lambda的默认参数
    func { println(it) }
}

没有对比就没有伤害,java费了十牛三虎之力写了好几行,kotlin短短几行就实现了,这就是lambda的优点: 简洁省事。其实说白了就是:不用创建对象了。

虽然可读性差了点,管它呢,反正看不懂也是别人的事,别人读不懂才能凸显我的不可替代性。

事实证明,lambda确实大大简化了代码的书写过程,我们不用敲创建对象的代码了

那么,lambda有什么缺点呢?

lambda的缺点

lambda的最大缺点就是性能损耗!

让我们反编译上述kotlin代码来看:

// 这个参数已经被替换成Function1了,这个Function1是kotlin中定义的一个接口
public static final void func(@NotNull Function1 action) {
    Intrinsics.checkNotNullParameter(action, "action");
    String str = "hello";
    action.invoke(str);
}

// main函数
public static final void main() {
    // 这里其实是创建了一个匿名类
    func((Function1)null.INSTANCE);
}

我们看到,kotlin中的lambda最终会在编译期变成一个匿名类,这跟java好像没什么区别啊,都是生成一个匿名类。为什么说kotlin的lambda效率低,因为:kotlin创建匿名类是在编译期

而java在1.7之后就引入了invokedynamic指令,java中的lambda在编译期会被替换为invokedynamic指令,在运行期,如果invokedynamic被调用,就会生成一个匿名类来替换这个指令,后续调用都是用这个匿名类来完成

说白了,对于java来说,如果lambda不被调用,就不会创建匿名类。而对于kotlin来说,不管lambda是否被调用,都会提前创建一个匿名类。这就等价于:java把创建匿名类的操作后置了,有需要才搞,这就变相节省了开销。因为创建匿名类会增加类个数和字节码大小。

那么,kotlin为什么不也这么干呢,为什么非要在编译时 就提前做 将来不一定用到的东西呢?因为kotlin需要兼容java6,java6是目前Android的主要开发语言,而invokedynamic又是在java7之后引入的...,mmp!

那么,kotlin怎么擦好这个屁股呢?使用内联函数!

内联函数的实现原理

还是上述代码,我们把func改成内联的,如下:

fun main() {
    func { print(it) }
}

// 方法用inline修饰了
inline fun func(action: (String) -> Unit) {
    val str = "hello"
    action.invoke(str)
}

同样,我们反编译下看看:

// 这个函数没变化
public static final void func(@NotNull Function1 action) {
    Intrinsics.checkNotNullParameter(action, "action");
    String str = "hello";
    action.invoke(str);
}

// 哦,调用方变了:直接把func函数体拷贝过来了,six six six
public static final void main() {
    String str$iv = "hello";
    System.out.print(str$iv);
}

我们看到,添加了inline后,kotlin会直接把被调用函数的函数体,复制到调用它的地方。

这样就不用创建匿名对象了!而且,还少一次调用过程。因为调用匿名对象的函数,本身还多一次调用呢。比如:

// 内联前
public void test(){
    A a = new a();
    a.hello(); //  这里调用一次hello()
}

// 内联后
public void test(){
    // a.hello()的代码直接拷贝进来,不用调hello()了!
}

所以,内联牛逼,万岁万岁万万岁。

但是,内联也有缺点!比如,我现在有个内联函数test(),里面有1000行代码,如果有10个地方调用它,那么就会把它复制到这10个地方,这一下就是10000行。。。这就导致class文件变相增大,进而导致apk变大,用户看见就不想下了。

怎么办呢,那就不内联!也就是说:根据函数的大小,以及被调用次数的多少,来决定是否需要内联

这是个业务的决策问题,这里不再废话。

内联函数的其他规则

好,我们来看下内联函数的一些规则。

内联函数的局限性

内联函数作为参数,只能传递给另一个内联函数。比如:

// func2是非内联的
fun func2(action: (String) -> Unit) {

}

// func是内联的
inline fun func(action: (String) -> Unit) {
    val str = "hello"
    action.invoke(str)

    // action此时是内联的,传递给非内联函数func2,就会报错
    func2(action) // 报错
}

现在我们讲func2改为内联的:

// 将func2改为内联
inline fun func2(action: (String) -> Unit) {

}

// func是内联的
inline fun func(action: (String) -> Unit) {
    val str = "hello"
    action.invoke(str)

    // 将action传递给另一个内联函数func2,正常
    func2(action) // ok
}

如果,不希望修改func2()为内联的怎么办呢,此时可以使用noinline修饰action参数:

// func2是非内联的
fun func2(action: (String) -> Unit) {

}

// func是内联的,但是action被标记为非内联的
inline fun func(noinline action: (String) -> Unit) {
    val str = "hello"
    action.invoke(str)

    // action此时是非内联的,可以传递给非内联函数func2
    func2(action) // ok
}

内联函数引的非局部返回

局部返回

我们知道,一般函数调用的返回都是局部的,比如:

// 这里直接return,也就是返回到调用它的地方
fun tReturn() {
    return
}

fun func() {
    println("before")
    // 调用了toRetrun()
    tReturn()
    println("after")
}

// 测试
fun main() {
    func()
}

结果如下:

before
after

这是正常的,因为func()函数先打印before,然后调用tReturn(),tReturn()入栈,执行return,tReturn()出栈,回到func()函数,接着向下打印after。

但是,如果将func()声明为内联的,然后将tReturn()作为参数传入,那么func()方法体就变了,比如:

// func声明为内联的,然后传入action参数
inline fun func(action: () -> Unit) {
    println("before")
    action.invoke()
    println("after")
}

fun main() {
    // 参数跟tReturn一样
    func { return }
}

结果:

before

原理也很简单,因为参数action会被复制到func()函数中,也就合并为一个方法了,等价于:

inline fun func() {
    println("before")
    return // 这就是参数action的函数体,直接返回了
    println("after")
}

这个不难理解,那么,如果不加inline,只是修改参数为action可以吗,比如:

// 这里没有加inline 参数一样是action
fun func(action: () -> Unit) {
    println("before")
    action.invoke()
    println("after")
}

fun main() {
    func { return } // 报错
}

这会直接报错:

Kotlin: 'return' is not allowed here

这是不允许的,因为它不知道你要return到哪个地方,但是可以这样写:

fun main() {
    // return 添加了标记,标记为返回到func这个地方
    func { return@func }
}

结果:

before
after

综上,一句话: 普通函数参数的return都是局部返回的,而内联函数是全局返回的

那么,怎么防备这种风险呢,或者说: 怎么让一个函数既可以内联,又不让它的参数有全局返回的return呢?比如:

inline fun func(action: () -> Unit) {
    println("before")
    action() // 希望这里不要有return,有就直接报错
    println("after")
}

使用crossinline即可!我们修改函数如下:

// 参数用crossinline修饰
inline fun func(crossinline action: () -> Unit) {
    println("before")
    action()
    println("after")
}

// 调用
fun main() {
    func { return } // 报错: Kotlin: 'return' is not allowed here
    func { return@func } // 正常
}

可以看到,corssinline在保证函数是内联的情况下,限制了全局返回

总结

  • kotlin为了书写简洁,引入了lambda
  • 但是lambda有性能开销
  • 性能开销在java7优化了,但是kotlin兼容java6,无法享受这个优化
  • 所以kotlin引入内联来解决这个问题
  • 内联是在编译期将被调用的函数拷贝到调用方的函数体,从而避免创建内部类
  • 使用inline可以将函数声明为内联的,内联函数参数是全局返回的
  • 使用noinline可以修饰函数参数为不内联
  • 使用crossinline可以修饰函数参数为内联,而且不能全局返回