协程代码生成-3.Inline(译)

Inline

将suspend函数内联化是一件棘手的事情。

在我们深入讨论之前,让我们解释一下,携带参数的内联函数存在64种可能的组合:

  1. 函数可以是suspend函数或普通函数
  2. 参数可以是 noinline、inline 或 crossinline lambda,也可以没有 lambda
  3. 参数可以是我们内联的块,也可以是我们无法内联的变量
  4. 参数可以是可挂起的或普通的
  5. 我们可以内联函数,也可以通过反射调用它

当然,其中一些是不正确的,因为在 Kotlin 中没有suspend的非函数参数。suspend参数只能是函数类型。此外,当我们通过反射调用时,无法内联变量。

让我们绘制一个表格,列出所有可能的组合,并逐行填写:

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|ordinary|noinline      |block        |ordinary      |inline   |no suspend call allowed                        |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|noinline      |block        |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|noinline      |variable     |ordinary      |inline   |no suspend call allowed                        |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|noinline      |variable     |ordinary      |call     |no suspend call allowed                        |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|noinline      |variable     |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|noinline      |variable     |suspend       |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |block        |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |block        |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |ordinary      |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |suspend       |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|crossinine    |block        |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|crossinine    |block        |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|crossinine    |variable     |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|crossinine    |variable     |ordinary      |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|crossinine    |variable     |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|crossinine    |variable     |suspend       |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|no lambda     |variable     |ordinary      |inline   |no suspend call allowed                        |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|no lambda     |variable     |ordinary      |call     |no suspend call allowed                        |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |block        |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |block        |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |ordinary      |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |suspend       |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |block        |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |block        |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |ordinary      |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |suspend       |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |block        |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |block        |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |variable     |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |variable     |ordinary      |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |variable     |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |variable     |suspend       |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |no lambda     |variable     |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |no lambda     |variable     |ordinary      |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

请注意,并非所有带有普通参数的普通函数都不在本文档的范围之外。

让我们解释一下 argument kind 中的 blockvariable 是什么意思。我们可以通过提供一个块或传递 lambda 类型的变量来调用带有 lambda 参数的内联函数:

kotlin 复制代码
inline fun inlineMe(c: () -> Unit) { c() }

fun main() {
    // block
    inlineMe { println("block") }

    // variable
    val variable = { println("block") }
    inlineMe(variable)
}

弄清楚这一点后,现在让我们考虑一下没有 lambda 参数的suspend内联函数。

Inline Suspend Functions with Ordinary Parameters

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|suspend |no lambda     |variable     |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |block        |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

suspend函数应该有一个状态机,除非它们是尾调用,并且我们不能将一个状态机内联到另一个状态机中,因为这会重置标签字段。

我们生成它们就像普通函数一样,但已经生成了所有的标记。换句话说,如果我们有一个内联suspend函数,像这样:

kotlin 复制代码
suspend fun returnsUnit() = suspendCoroutine<Unit> { it.resume(Unit) }

suspend inline fun inlineMe() {
    returnsUnit()
}

编译器生成的字节码如下:

text 复制代码
ALOAD 1 // continuation
ICONST 0 // before suspending marker
INVOKESTATIC InlineMarker.mark
INVOKESTATIC returnsUnit()
ICONST_2 // returns unit marker
INVOKESTATIC InlineMarker.mark
ICONST 1 // after suspending marker
INVOKESTATIC InlineMarker.mark
POP
GETSTATIC kotlin/Unit.INSTANCE
ARETURN

如果仔细观察,这恰好是如果我们没有声明函数为内联时传递给 CoroutineTransformerMethodVisitor 的字节码,但没有堆栈溢出标记,因为内联器会为我们溢出堆栈。

现在,让我们再创建另一个内联函数,调用之前的函数。

kotlin 复制代码
suspend inline fun inlineMe2() {
    inlineMe()
    inlineMe()
}

生成的字节码如下所示:

text 复制代码
ALOAD 1 // continuation
ICONST 0 // before suspend marker
INVOKESTATIC InlineMarker.mark
INVOKESTATIC returnsUnit()
ICONST_2 // returns unit marker
INVOKESTATIC InlineMarker.mark
ICONST 1 // after suspend marker
INVOKESTATIC InlineMarker.mark
POP

ALOAD 1 // continuation
ICONST 0 // before suspend marker
INVOKESTATIC InlineMarker.mark
INVOKESTATIC returnsUnit()
ICONST_2 // returns unit marker
INVOKESTATIC InlineMarker.mark
ICONST 1 // after suspend marker
INVOKESTATIC InlineMarker.mark
POP

GETSTATIC kotlin/Unit.INSTANCE
ARETURN

正如您所见,它内联了内联函数。但是,它不会生成状态机,因为该函数稍后将被内联。请注意,编译器不会在内联的字节码周围生成挂起标记,因为状态机应该是平的,所以不允许嵌套的挂起点。

当 lambda 参数为普通参数时,在参数调用周围没有标记。

现在我们可以填写表格:

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|suspend |no lambda     |variable     |ordinary      |inline   |no state-machine with markers                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |block        |ordinary      |inline   |no state-machine with markers                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |ordinary      |inline   |no state-machine with markers                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

Java Interop and Reflection(Java 互操作和反射)

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|suspend |no lambda     |variable     |ordinary      |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |ordinary      |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

然而,这并不简单。协程内联以其复杂性而著称,这主要是因为它需要支持的各种边界情况。与Java的后向互操作性(从Java调用Kotlin代码)就是其中之一。应该可以从Java调用内联函数。由于javac不会内联它们,生成的代码直接调用它们。是的,甚至是内联挂起函数。支持后向兼容性的原因很简单:反射的工作方式与从Java调用相同。它不会内联函数,而是直接调用它们。对于内联挂起函数来说,这意味着当我们使用反射或Java调用该函数时,它应该具有状态机。然而,另一方面,我们不应该这样做,因为我们无法将一个状态机内联到另一个状态机中。

因此,我们不能仅仅生成一个函数就足够了。相反,我们生成内联函数的两个版本:一个用于内联器,另一个用于直接调用和反射调用。用于直接调用的版本名称保持不变,并具有状态机和continuation。用于内联器的版本没有状态机,但具有标记的挂起调用,并带有$$forInline后缀。

例如:

kotlin 复制代码
suspend fun returnsUnit() = suspendCoroutine<Unit> { it.resume(Unit) }

suspend inline fun inlineMe() {
    returnsUnit()
}

suspend inline fun inlineMe2() {
    inlineMe()
    inlineMe()
}

编译器生成以下方法:

text 复制代码
// Can be called directly, have state-machine, unless tail-call
public returnsUnit(L/kotlin/coroutines/Continuation;)Ljava/lang/Object;
public inlineMe(L/kotlin/coroutines/Continuation;)Ljava/lang/Object;
public inlineMe2(L/kotlin/coroutines/Continuation;)Ljava/lang/Object;
// For inliner use only, no state-machine, suspend calls are marked
private inlineMe$$forInline(L/kotlin/coroutines/Continuation;)Ljava/lang/Object;
private inlineMe2$$forInline(L/kotlin/coroutines/Continuation;)Ljava/lang/Object;

注意,$$forInline 版本具有私有可见性,因此像 ProGuard 这样的工具可以轻松删除它们。这种函数复制是为了保持代码的语义,无论是内联函数还是通过反射调用。

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|suspend |no lambda     |variable     |ordinary      |call     |state-machine                                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |ordinary      |call     |state-machine                                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

Inline-Only Functions

对于规则的一个例外是仅内联的函数。例如,如果它们带有 @kotlin.internal.InlineOnly 注解或具有具体化的类型参数。由于无法通过反射或从Java调用它们,因此我们不需要对它们进行复制。因此,只有一个版本 - 供内联器使用。但是,由于没有反射版本,我们不需要对这些函数进行处理。换句话说,我们保留函数的名称。例如,如果我们有一个仅内联的函数:

kotlin 复制代码
suspend fun blackhole() {}

suspend inline fun <reified T> inlineMe(t: T): T {
    blackhole()
    return t
}

生成的字节码将如下所示:

text 复制代码
synthtic public inlineMe(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
ALOAC 0 // parameter
ALOAD 1 // continuation
ICONST_0 // before suspend call marker
INVOKESTATIC kotlin/jvm/internal/InlineMarker.mark
INVOKESTATIC blackhole (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
ICONST_2 // returns Unit marker
INVOKESTATIC kotlin/jvm/internal/InlineMarker.mark
ICONST_1 // after suspend call marker
INVOKESTATIC kotlin/jvm/internal/InlineMarker.mark
POP
ARETURN

Ordinary Inline Functions(普通内联函数)

由于普通内联函数没有状态机,它们既可以直接调用,也可以内联。因此,对于普通函数来说,"call" 和 "inline" 的调用种类之间没有区别。这进一步减少了不同组合的数量。

然而,有一个值得注意的例外。

Ordinary Inline Parameter Of Ordinary Inline Function

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|ordinary|inline        |block        |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |ordinary      |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

假设我们有以下代码:

kotlin 复制代码
suspend fun isFoo(): Boolean = TODO()

suspend fun main() {
    println(listOf(1, 2, 3).filter { isFoo() })
}

正如你所看到的,我们在普通内联函数的普通内联 lambda 块参数中使用了一个挂起函数。内联器将 lambda 内联到函数中,然后将函数内联到调用站点,调用站点是一个挂起上下文。因此,我们应该支持这种情况。这样,我们就可以从上下文中检索到 continuation,并将其传递给调用。请注意,如果函数或参数未被内联,上下文中将没有 continuation。因此,我们禁止这些情况。

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|ordinary|inline        |block        |ordinary      |inline   |suspend calls in block allowed                 |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |ordinary      |inline   |no suspend calls allowed                       |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |ordinary      |call     |no suspend calls allowed                       |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

Ordinary Inline Parameter of Suspend Inline Function

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|suspend |inline        |block        |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |ordinary      |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |ordinary      |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

由于 lambda 被内联,上下文中存在 continuation。因此,我们可以调用挂起函数。而且,这是一个挂起内联函数。因此我们对其进行了复制。

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|suspend |inline        |block        |ordinary      |inline   |suspend calls in block allowed,                |
|        |              |             |              |         |markers without state-machine                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |ordinary      |inline   |no suspend call in argument allowed,           |
|        |              |             |              |         |markers without state-machine                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |ordinary      |call     |no suspend call in argument allowed,           |
|        |              |             |              |         |state-machine                                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

Suspend Inline Parameter of Suspend Inline Function

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|suspend |inline        |block        |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |suspend       |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

如果将参数内联到函数中,则 lambda 的调用将使用函数的 continuation。因此,将其定义为挂起是没有必要的。因此,我们对具有内联挂起 lambda 参数的挂起内联函数生成警告,要求程序员删除 suspend 关键字。程序员可以在其他情况下忽略此警告。由于最常见的情况是内联,因此警告是有意义的。我们决定在发现存在不内联的情况时保留此警告。

挂起点不能嵌套,因此我们生成不同的挂起标记:内联挂起标记。如果内联器将 lambda 内联,则它们将被移除;如果没有 lambda 内联,则会被真实的挂起标记替换。

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|suspend |inline        |block        |suspend       |inline   |WARNING to remove `suspend` keyword            |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |suspend       |inline   |WARNING should be ignored,                     |
|        |              |             |              |         |no state-machine with markers                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |suspend       |call     |WARNING should be ignored, state-machine       |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

Inline Suspend Parameter of Ordinary Inline Function

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|ordinary|inline        |block        |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |suspend       |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

因为挂起 lambda 参数的主体应该内联到普通内联函数的主体中,而后者又可以内联到非挂起上下文中,所以我们无法检索 continuation 并将其传递给 lambda 可能具有的挂起调用。换句话说,我们禁止这种组合。

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|ordinary|inline        |block        |suspend       |inline   |FORBIDDEN                                      |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |suspend       |inline   |FORBIDDEN                                      |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |suspend       |call     |FORBIDDEN                                      |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

Noinline Suspend Parameter of Suspend Inline Function

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|suspend |noinline      |block        |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |suspend       |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

在我们讨论内联挂起 lambda 或交叉内联挂起 lambda 之前,让我们看一下内联函数的非内联挂起 lambda 参数是如何工作的。例如,如果我们甚至有一个可能的最简单的函数:

kotlin 复制代码
suspend inline fun inlineMe(noinline c: suspend () -> Unit) { c() }

我们需要用挂起标记包装参数的调用;否则,调用将不会在状态机中具有状态。因此,编译器生成的代码如下所示:

text 复制代码
ALOAD 0 // lambda 参数
ALOAD 1 // continuation
ICONST_0 // 挂起调用前的标记
INVOKESTATIC kotlin/jvm/internal/InlineMarker.mark
INVOKEINTERFACE Function1.invoke (Ljava/lang/Object;)Ljava/lang/Object;
ICONST_2 // 返回 Unit 的标记
INVOKESTATIC kotlin/jvm/internal/InlineMarker.mark
ICONST_1 // 挂起调用后的标记
INVOKESTATIC kotlin/jvm/internal/InlineMarker.mark
POP
GETSTATIC kotlin/Unit.INSTANCE
ARETURN

当然,对于 call,我们也生成状态机。

请注意,我们可以在内部 lambda 或对象内调用 noinline 参数,就像下面这样:

kotlin 复制代码
suspend inline fun inlineMe(noinline c: suspend () -> Unit) = suspend { c() }

在这种情况下,我们只为 lambda 生成状态机,并且当内联函数内联时,内联器会复制它。

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|suspend |noinline      |block        |suspend       |inline   |if called in function, markers around invoke   |
|        |              |             |              |         |if called in inner function, state-machine     |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |suspend       |inline   |if called in function, markers around invoke   |
|        |              |             |              |         |if called in inner function, state-machine     |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |suspend       |call     |state-machine with parameter invoke in a state |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

Noinline Suspend Parameter of Ordinary Inline Function

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|ordinary|noinline      |block        |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|noinline      |variable     |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|noinline      |variable     |suspend       |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

为了调用挂起的非内联参数,我们需要一个挂起上下文。最简单的方法是将调用包装在一个挂起 lambda 中:

kotlin 复制代码
inline fun inlineMe(noinline c: suspend () -> Unit) = suspend { c() }

结果与前一节相同:调用位于一个单独的状态内,但这次它不在函数本身内,而是在 lambda 内部。由于这是一个普通的内联函数,它没有状态机,而 lambda 有。当内联函数内联时,内联器会复制它的类,并保留状态机。这样,无论是内联函数还是调用函数,语义都是相同的。

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|ordinary|noinline      |block        |suspend       |inline   |state-machine with parameter invoke in a state |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|noinline      |variable     |suspend       |inline   |state-machine with parameter invoke in a state |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|noinline      |variable     |suspend       |call     |state-machine with parameter invoke in a state |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

Crossroutines

最后,协程代码生成中最棘手的部分:在挂起函数或 lambda 中捕获的跨内联 lambda,或称为"交叉协程"。

让我们从一个简单的例子开始:

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|ordinary|crossinine    |block        |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
kotlin 复制代码
interface SuspendRunnable {
    suspend fun run()
}

suspend fun dummy() {}

inline fun crossinlineMe(crossinline c: suspend () -> Unit): SuspendRunnable =
    object : SuspendRunnable {
        override suspend fun run() {
            dummy()
            c()
        }
    }

The compiler generated code for the inline function will look like

kotlin 复制代码
public final class crossilineMe$1($captured_local_variable$0: Function1<Continuation<Unit>, Unit>) {
    final /*package-local*/ $c : Function1<Continuation<Unit>, Unit>

    init {
        $c = $captured_local_variable$0
    }

    public override fun run($completion: Continuation) {
        beforeFakeContinuationConstructionCall()
        run$1()
        afterFakeContinuationConstructionCall()
        dummy()
        this.$c.invoke($completion)
    }

    public final class run$1 {
        // Partially built continuation.
    }
}

public final fun crossinlineMe(c: Function1<Continuation<Unit>, Unit>): SuspendRunnable {
    return crossinlineMe$1(c)
}

当内联器内联 crossinlineMe 函数时,它首先将其 lambda 内联到对象中(这是称为匿名对象转换的过程),替换对象的使用,最后再内联函数。

注意 run$1 类。它是由代码生成器为内联器生成的"假continuation",也是一个continuation类。如果没有它,内联器将需要生成continuation类。但是,有了假continuation,它只需要转换假continuation,就像转换任何其他内部对象一样。假continuation就是一个没有溢出变量的continuation类。

For example, if we use the function, like:

kotlin 复制代码
suspend fun main() {
    crossinlineMe {
        pritln("OK")
    }.run()
}

Firstly, we transform the object:

kotlin 复制代码
public final class main$1 {
    public override fun run($completion: Continuation) {
        val $continuation = if ($completion is run$1 && $completion.label.sign_bit.is_set) {
            $completion.label.sign_bit.unset
            $completion
        } else {
            run$1($completion)
        }
        when ($continuation.label) {
            0 -> {
                dummy()
                // error handling is omitted
            }
            1 -> {
                println("OK")
                return
            }
            else -> {
                error(...)
            }
        }
    }

    public final run$1: Continuation {
        // Continuation's content
    }
}

正如你所看到的,我们在转换过程中生成了状态机,并且内联器转换了假continuation。状态机构建器移除了假continuation的构造函数调用。为此,它查找假continuation构造函数调用前后的标记,它们的值分别为 45

然后,我们替换对象的使用:

kotlin 复制代码
public final fun tmp(): SuspendRunnable {
    return main$1()
}

最后,我们内联该函数:

kotlin 复制代码
public final fun main$$$($completion: Continuation<Unit>) {
    main$1().run()
}

既然我们将 lambda 内联到 run 函数中,我们无法在代码生成期间为其生成状态机,只能在转换期间生成。

由于参数声明为 suspend,而函数本身则不是,我们无法在函数内部调用参数;我们需要有一些挂起上下文,即内部 lambda 或内部具有挂起函数的对象。

请注意,如果 lambda 不包含挂起调用,我们不应该将内联的 lambda 放入单独的状态中;换句话说,我们应该移除围绕内联代码的挂起标记。为此,我们引入了新的挂起标记:内联挂起调用前后的标记。它们的值分别为 67。如果 lambda 被内联,那么在 lambda 被内联后,在构建状态机之前,内联器会移除这些标记。否则,如果参数未被内联,则它会将内联挂起标记替换为真实的挂起标记。

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|ordinary|crossinine    |block        |suspend       |inline   |inline suspend markers around invoke           |
|        |              |             |              |         |fake continuation for inliner to transform     |
|        |              |             |              |         |state-machine is built after inlining          |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

现在假设我们不内联参数:

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|ordinary|crossinine    |variable     |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|crossinine    |variable     |suspend       |call     |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

我们无法调用该函数,因为它在对象的挂起函数内部没有状态机。需要状态机和不需要状态机之间的这种困境与内联挂起函数相同。因此,解决方案也相同:我们复制挂起函数,在这种情况下是 run

kotlin 复制代码
public final class crossilineMe$1($captured_local_variable$0: Function1<Continuation<Unit>, Unit>) {
    final /*package-local*/ $c : Function1<Continuation<Unit>, Unit>

    init {
        $c = $captured_local_variable$0
    }

    public override fun run$$forInline($completion: Continuation) {
        beforeFakeContinuationConstructionCall()
        run$1()
        afterFakeContinuationConstructionCall()
        dummy()
        this.$c.invoke($completion)
    }

    public override fun run($completion: Continuation) {
        val $continuation = if ($completion is run$1 && $completion.label.sign_bit.is_set) {
            $completion.label.sign_bit.unset
            $completion
        } else {
            run$1($completion)
        }
        when ($continuation.label) {
            0 -> {
                dummy()
                // error handling is omitted
            }
            1 -> {
                println("OK")
                return
            }
            else -> {
                error(...)
            }
        }
    }

    public final run$1: Continuation {
        // Continuation's content
    }
}

public final fun crossinlineMe(c: Function1<Continuation<Unit>, Unit>): SuspendRunnable {
    return crossinlineMe$1(c)
}

FIXME: 我们不再需要假continuation,除非函数是尾调用或仅内联。内联器会转换 run 的continuation,它可以从 run 的函数头中获取continuation。

正如你所见,我们有两个版本:一个带有状态机,当内联器转换对象时会被丢弃,另一个用于内联,作为生成"真正"状态机的模板。

在挂起的 lambda 的情况下,我们会复制其 invokeSuspend 方法。

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|ordinary|crossinine    |variable     |suspend       |inline   |suspend function duplication                   |
|        |              |             |              |         |inline suspend markers replaced with real ones |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|crossinine    |variable     |suspend       |call     |suspend function duplication                   |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

当我们将 crossinlineMe 函数内联到另一个内联函数中时会发生什么呢:

kotlin 复制代码
inline fun crossinlineMe2(crossinline c: suspend () -> Unit): SuspendRunnable =
    object : SuspendRunnable {
        override suspend fun run() {
            crossinlineMe { c() }
        }
    }

在这种情况下,我们不能简单地丢弃状态机模板(即传递给状态机构建器的函数版本)。因此,如果内联器将函数内联到另一个内联函数中,它会保留模板(当然,在内联之后)。

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|ordinary|crossinine    |block        |suspend       |inline   |inline suspend markers around invoke           |
|        |              |             |              |         |if inline-site is inline, keep the template    |
|        |              |             |              |         |fake continuation for inliner to transform     |
|        |              |             |              |         |state-machine is built after inlining          |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

Let us now make the function itself suspend:

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|suspend |crossinine    |block        |suspend       |inline   |                                               |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
kotlin 复制代码
suspend inline fun crossinlineMe(crossinline c: suspend () -> Unit): SuspendRunnable =
    object : SuspendRunnable {
        override suspend fun run() {
            c()
        }
    }

代码生成器当然会将其复制,但它不会复制对象。因此,它仅仅是内联挂起和交叉内联挂起的结合。

有一个区别。你看,就像noinline参数一样,crossinline参数可以在函数本身中被调用:

kotlin 复制代码
suspend inline fun inlineMe(crossinline c: suspend () -> Unit) {
    c()
}

在这个例子中,尽管inlineMe的参数被声明为crossinline,但它是内联的。顺便说一句,这就是suspendCoroutineUninterceptedOrReturnsuspendCoroutine的参数的行为。

由于参数是内联的,行为与没有crossinline关键字时相同,但没有多余的挂起修饰符警告。

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|suspend |crossinine    |block        |suspend       |inline   |inline suspend markers around invoke           |
|        |              |             |              |         |if inline-site is inline, keep the template    |
|        |              |             |              |         |fake continuation for inliner to transform     |
|        |              |             |              |         |state-machine is built after inlining          |
|        |              |             |              |         |function is duplicated                         |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

为了简化挂起函数转换的流程,因为它已经够复杂的了,我们不支持以不同方式处理普通的crossinline参数。如果它的调用点是挂起的,我们会对其进行复制,然后再转换对象。

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|ordinary|crossinine    |variable     |ordinary      |inline   |if lambda call-site is suspend, duplicate it   |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |block        |ordinary      |inline   |if lambda call-site is suspend, duplicate it   |
|        |              |             |              |         |function is duplicated                         |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |variable     |ordinary      |inline   |if lambda call-site is suspend, duplicate it   |
|        |              |             |              |         |function is duplicated                         |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |variable     |ordinary      |call     |if lambda call-site is suspend, duplicate it   |
|        |              |             |              |         |function is duplicated                         |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+

Summary

以下是所有可能组合的完整列表,不包括无效的组合。现在可以看到为什么协程内联是一个棘手的问题。

text 复制代码
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|function|parameter kind|argument kind|parameter type|call kind|notes                                          |
+========+==============+=============+==============+=========+===============================================+
|ordinary|noinline      |block        |ordinary      |inline   |no suspend call allowed                        |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|noinline      |block        |suspend       |inline   |state-machine with parameter invoke in a state |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|noinline      |variable     |ordinary      |inline   |no suspend call allowed                        |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|noinline      |variable     |ordinary      |call     |no suspend call allowed                        |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|noinline      |variable     |suspend       |inline   |state-machine with parameter invoke in a state |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|noinline      |variable     |suspend       |call     |state-machine with parameter invoke in a state |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |block        |ordinary      |inline   |suspend calls in block allowed                 |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |block        |suspend       |inline   |FORBIDDEN                                      |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |ordinary      |inline   |no suspend calls allowed                       |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |ordinary      |call     |no suspend calls allowed                       |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |suspend       |inline   |FORBIDDEN                                      |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|inline        |variable     |suspend       |call     |FORBIDDEN                                      |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|crossinine    |block        |ordinary      |inline   |no suspend call allowed                        |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|crossinine    |block        |suspend       |inline   |inline suspend markers around invoke           |
|        |              |             |              |         |if inline-site is inline, keep the template    |
|        |              |             |              |         |fake continuation for inliner to transform     |
|        |              |             |              |         |state-machine is built after inlining          |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|crossinine    |variable     |ordinary      |inline   |if lambda call-site is suspend, duplicate it   |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|crossinine    |variable     |ordinary      |call     |if lambda call-site is suspend, duplicate it   |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|crossinine    |variable     |suspend       |inline   |suspend function duplication                   |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|crossinine    |variable     |suspend       |call     |suspend function duplication                   |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|no lambda     |variable     |ordinary      |inline   |no suspend call allowed                        |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|ordinary|no lambda     |variable     |ordinary      |call     |no suspend call allowed                        |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |block        |ordinary      |inline   |no state-machine with markers                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |block        |suspend       |inline   |if called in a function, markers around invoke |
|        |              |             |              |         |if called in inner function, state-machine     |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |ordinary      |inline   |no state-machine with markers                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |ordinary      |call     |state-machine                                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |suspend       |inline   |if called in a function, markers around invoke |
|        |              |             |              |         |if called in inner function, state-machine     |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |noinline      |variable     |suspend       |call     |state-machine with parameter's invoke in state |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |block        |ordinary      |inline   |suspend calls in block allowed,                |
|        |              |             |              |         |markers without state-machine                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |block        |suspend       |inline   |WARNING to remove `suspend` keyword            |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |ordinary      |inline   |no suspend call in argument allowed,           |
|        |              |             |              |         |markers without state-machine                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |ordinary      |call     |no suspend call in argument allowed,           |
|        |              |             |              |         |state-machine                                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |suspend       |inline   |WARNING should be ignored,                     |
|        |              |             |              |         |no state-machine with markers                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |inline        |variable     |suspend       |call     |WARNING should be ignored, state-machine       |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |block        |ordinary      |inline   |if lambda call-site is suspend, duplicate it   |
|        |              |             |              |         |function is duplicated                         |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |block        |suspend       |inline   |inline suspend markers around invoke           |
|        |              |             |              |         |if inline-site is inline, keep the template    |
|        |              |             |              |         |fake continuation for inliner to transform     |
|        |              |             |              |         |state-machine is built after inlining          |
|        |              |             |              |         |function is duplicated                         |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |variable     |ordinary      |inline   |if lambda call-site is suspend, duplicate it   |
|        |              |             |              |         |function is duplicated                         |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |variable     |ordinary      |call     |if lambda call-site is suspend, duplicate it   |
|        |              |             |              |         |function is duplicated                         |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |variable     |suspend       |inline   |suspend function duplication                   |
|        |              |             |              |         |function is duplicated                         |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |crossinine    |variable     |suspend       |call     |suspend function duplication                   |
|        |              |             |              |         |function is duplicated                         |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |no lambda     |variable     |ordinary      |inline   |no state-machine with markers                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
|suspend |no lambda     |variable     |ordinary      |call     |state-machine                                  |
+--------+--------------+-------------+--------------+---------+-----------------------------------------------+
相关推荐
heeheeai5 小时前
kotlin kmp 副作用函数 effect
kotlin·effect·kmp·副作用函数
纳于大麓6 小时前
Kotlin基础语法一
android·开发语言·kotlin
KotlinKUG贵州10 小时前
Spring开发,从Kotlin开始
spring boot·spring·kotlin
移动开发者1号13 小时前
Android中Activity、Task与Process的关系
android·kotlin
移动开发者1号14 小时前
Activity onCreate解析
android·kotlin
alexhilton1 天前
在Android应用中实战Repository模式
android·kotlin·android jetpack
雨白1 天前
高阶函数与内联优化
kotlin
&岁月不待人&1 天前
实现弹窗随键盘上移居中
java·kotlin
移动开发者1号2 天前
Android Activity状态保存方法
android·kotlin
移动开发者1号2 天前
Volley源码深度分析与设计亮点
android·kotlin