简单理解 Kotlin 中的 inline 关键字

参考:

inline 这个词其实很好理解,翻译成中文就指内联。

相信每一个学习 Kotlin 的同学都有用过或者遇见过 inline 这个关键字。即便没有在 Kotlin 中见过那也不是什么问题,本文将带领你理解这个关键词。

实际上,如果你有学过C++,那你已经知道它是个什么玩意了。

本文就 Kotlin 中的 inline 进行讲解。

inline 是如何工作的

在此之前,我想先谈一谈 inline 与 lambda

Kotlin 在许多情况下使用编译器技巧来隐藏 Java 的旧语言结构。

在使用函数作为另一个函数的参数(所谓的高阶函数)比在Java中更自然。

虽然 Kotlin 好处多多,但在用 lambda 时有一些缺点。由于 lambda 表达式都会被悄悄的编译成一个匿名类。这也就意味着需要占用内存。如果短时间内 lambda 表达式被多次调用,大量的对象实例化就会产生内存流失(Memory Churn)。

所以为了避免这种情况,我们就可以使用 inline

  inline fun inlined(getString: () -> String?) = println(getString())

  fun notInlined(getString: () -> String?) = println(getString())

上面的代码中,一个是被 inline 修饰的内联函数,另一个不是。虽然他们的业务解决结果完全相同,都是打印getString(),但如果你实际调用了这两个函数,从反编译后的Java代码中就可以看到很大的差异。

实际调用:

  fun test() {
    var testVar = "Test"

    notInlined { testVar }

    inlined { testVar }
}

相应的反编译的Java代码:

  public static final void test() {
    final ObjectRef testVar = new ObjectRef();
    testVar.element = "Test Variable";

    // notInlined:
    notInlined((Function0)(new Function0(0) {
        public Object invoke() {
            return this.invoke();
        }

        @NotNull
        public final String invoke() {
           return (String)testVar.element;
        }
    }));

    // inlined:
    String var3 = (String)testVar.element;
    System.out.println(var3);

从反编译的代码来看,我们并没有找到实际调用inlined函数的迹象。

notInlined 使用 Function0 匿名类作为参数调用了该函数。为了遵守Function0接口的协议所以就使用invoke()实现了该方法。由于接口是通用的,因此生成另外的方法,即所谓的桥接方法。

通过上面的示例,你应该大概也看明白了

inline 的工作原理就是将内联函数的函数体复制到调用处实现内联。

inlined 函数内的(也就是 inlined 函数等号右边的) printlin(),被拿去直接 System.out.println()

所以如果我们的方法比较大或者调用的比较多的话,那么编译器生成的代码量就会变大。

值得注意的是,如果方法变得足够大,过度使用 inline 可能会妨碍或停止 Hotspot 优化(例如方法内联)。默认情况下, Hotspot 不会内联大于35个字节的方法。

我们不应该内联所有功能。而且官方也不建议这样做。

在 kotlin-style-guide 这个库里的 issue 中找到个来自 Kotlin 贡献者的建议,
原文是:「Functions should only be made inline when they use inline-only features like inlined lambda parameters or reified types.」意思就是说:inline 关键字应该只用在需要内联特性的函数中,比如高阶函数作为参数和具体化的类型参数时。

当然了,官方也都是挺遵守这个规定的。比如在 Anko 库中,一个我很爱用的内联函数 bg 就有遵守着这个规定:

  @PublishedApi
  internal var POOL = newFixedThreadPoolContext(2 * Runtime.getRuntime().availableProcessors(), "bg")

  inline fun <T> bg(crossinline block: () -> T): Deferred<T> = async(POOL) {
    block()
}

要稍微注意一下的是 POOL 这个变量前面有 @PublishedApi 以及 internal 修饰着,这是因为内联函数体中不能直接访问到其外部类的成员,所以需要声明访问的成员为 internal 并且使用@PublishedApi做注解。

相信看到这里,你已经对 inline 有了基本了解。

推荐阅读更多精彩内容