挂起函数的返回值

690 阅读3分钟

返回值类型

协程中挂起函数的返回值类型是 Object,无论代码中写的是什么。

我们写的协程代码编译成 Java 代码后,挂起函数的返回值类型就会被修改成 Object,如下:

// 定义一个挂起函数,其返回值类型是 Int
private suspend fun test2(): Int {...}

// javap -v 反编译对应的 class 文件
.method private final test2(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

通过反编译可以看到,挂起函数额外多了个 Continuation 类型的参数,返回值类型也变成了 Object。关于前者它是协程能恢复的关键,是协程底层原理的基础知识,此处忽略。对于后者是本文重点。

返回值类型被修改的原因

调用到挂起函数时会返回特殊对象 COROUTINE_SUSPENDED,最终也会返回自己定义的返回值。

一个挂起函数会被调用多次,当它执行到另一个挂起函数时会返回 COROUTINE_SUSPENDED 给调用者。执行到函数最后时,它会返回该返回的值给调用者。因此,挂起函数会返回两种类型的数据,所以返回结果型只能是 Object 类型。

验证

为验证上面结论,以下面代码为例说明

private suspend fun test2(): Int {
    // withContext 是挂起函数
    val a = withContext(Dispatchers.IO) {
        delay(100)
        1
    }
    return 1 + a
}

首先通过 as 自带的 show kotlin bytecode 查看上述代码对应的 java 代码,如下

Xnip2023-06-28_15-44-46.png

关于 if 判断是否成立,可以直接反编译生成的 apk,向 apk 中插入代码,可以发现它和 var5 是同一个对象,所以 if 判断成立,因此此时 test() 返回的是 COROUTINE_SUSPENDED。

现在确定下上图中的 $continuation 到底是什么类型,反编译 apk 查看 smali 代码,可以看到 $continuation 其实是 MainActivity$test2$1 类型。

// test2 定义在 MainActivity 类中,所以生成的内部类都是 MainActivity$ 开头

new-instance v0, Lcom/example/demo/MainActivity$test2$1;
invoke-direct {v0, p0, p1}, Lcom/example/demo/MainActivity$test2$1;-><init>(Lcom/example/demo/
MainActivity;Lkotlin/coroutines/Continuation;)V
:goto_0
move-object p1, v0
.local p1, "$continuation":Lkotlin/coroutines/Continuation;

MainActivity$test2$1 继承 ContinuationImpl,最核心代码是它的 invokeSuspend(),对应的 smali 代码如下,看懂它的代码有助于我们理解 test2() 第二次执行逻辑:

.method public final invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
    .locals 2
    // 将 p1 赋值给 p0 的 result 中
    // p0 是当前对象。invokeSuspend() 非 static 函数,默认有一个参数 this,即 p0
    // 这句代码就是:将参数赋值给当前对象的 result 字段
    iput-object p1, p0, Lcom/example/demo/MainActivity$test2$1;->result:Ljava/lang/Object;
    
    // v0 = p0.label。即将当前对象的 label 赋值给 v0
    iget v0, p0, Lcom/example/demo/MainActivity$test2$1;->label:I
    
    // v1 = Int.MIN_VALUE
    const/high16 v1, -0x80000000
    
    // v0 与 v1 或运算,并将结果存储至 v0
    or-int/2addr v0, v1
    // 将 v0 赋值给 this.label
    iput v0, p0, Lcom/example/demo/MainActivity$test2$1;->label:I

    // this$0 是 jvm 中内部类添加的一个字段,用于表示外问类的引用,此处即 MainActivity 对象
    // 这句话就是将 MainActivity 赋值给 v0
    iget-object v0, p0, Lcom/example/demo/MainActivity$test2$1;->this$0:Lcom/example/demo/MainActivity;
    
    // 用 v1 指向当前对象,即 v1 = this 
    move-object v1, p0
    // 判断 v1 是不是 instanceof Continuation,肯定成立
    check-cast v1, Lkotlin/coroutines/Continuation;
    
    // 调用 MainActivity 的静态方法 access$test2,同时传入参数 MainActivity 实例
    // 以及当前类对象
    invoke-static {v0, v1}, Lcom/example/demo/MainActivity;->access$test2(Lcom/example/demo/MainActivity;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
    
    // 将上面 access$test2() 执行结果赋值给 v0
    move-result-object v0

    // 返回 v0,也就是返回 access$test2() 的执行结果
    return-object v0
.end method

在这段代码的最开始会将参数赋值给对象的 result 属性,结合验证一节中的截图 $result 字段,看一下它的赋值,就可以明白为啥 $result 取到的是挂起函数的返回值了。

上面代码提到了 MainActivity 的静态方法 access$test2 方法,看一眼,代码更简单:

.method public static final synthetic access$test2(Lcom/example/demo/MainActivity;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
    .locals 1
    .param p0, "$this"    # Lcom/example/demo/MainActivity;
    .param p1, "$completion"    # Lkotlin/coroutines/Continuation;

    .line 16
    // 直接执行 MainActivity 的 test2() 方法
    invoke-direct {p0, p1}, Lcom/example/demo/MainActivity;->test2(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
    // 同时将 test2() 的返回值直接返回
    move-result-object v0

    return-object v0
.end method

目前可知 test2() 由 invokeSuspend() 调用的,那该方法是由谁调用的呢?根据协程的基础知识可知,协程的恢复都是由它的 resumeWith() 开始的,该方法定义在 BaseContinuationImpl 中,如下:

Xnip2023-06-28_18-52-11.png

上图中会调用 invokeSuspend(),也就是调用本节分析的 invokeSuspend() 方法,最终会执行到 test2() 方法,拿到 test2() 的最终返回值。结合 while 死循环,最终会执行到 test3() 后面的步骤。

以上就是协程的挂起恢复流程,也说明了挂起函数的返回值为啥是 Object。