注册

挂起函数的返回值

返回值类型



协程中挂起函数的返回值类型是 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。


作者:鱼洗竹
链接:https://juejin.cn/post/7249633061440880700
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册