网上有许多挂起函数的原理,但都讲的太过深入或者示例太复杂,对新手很不友好,作为一个有理想的菜鸟,我将用最简单的示例让大家用几分钟的时间理解挂起函数的原理。
被suspend关键字修饰的函数被称为挂起函数,字面意思就是可以被挂起的函数,挂起可以理解为暂停的意思,能暂停就能恢复。
那挂起函数是怎么实现代码的暂停和恢复的呢?
如下:我们随便定义一个挂起函数,然后使用Android Studio 将kotlin反编译成Java代码。(反编译步骤:Tools->Kotlin->Show Kotlin Bytecode->Decompile)
suspend fun getInfo() : String {
delay(1000L)
Log.e("---" , "延迟后执行的代码")
return "hello world!"
}
反编译后如下:暂时先省略方法里面的代码
public final Object getInfo(@NotNull Continuation var1) {
...
}
可以看到,反编译后的代码 多了一个Continuation参数,并且返回值变成了Object,我们查看一下Continuation,如下是一个接口,有一个CoroutineContext的接口属性和一个resumeWith的接口方法。
public interface Continuation<in T> {
/**
* The context of the coroutine that corresponds to this continuation.
*/
public val context: CoroutineContext
/**
* Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
* return value of the last suspension point.
*/
public fun resumeWith(result: Result<T>)
}
翻译备注可以知道,resumeWith的作用是:继续执行相应的协程,将成功或失败的结果传递为最后一个暂停点的返回值。
所以:Continuation 其实就是一个带有泛型参数的 CallBack回调接口。这个从挂起函数转换成CallBack 函数的过程,被称为:CPS 转换(Continuation-Passing-Style Transformation)
那么这个回调接口有什么作用呢?
我们查看完整的反编译代码:
@Nullable
public final Object getInfo(@NotNull Continuation var1) {
Object $continuation;
label20: {
if (var1 instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)var1;
if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
break label20;
}
}
$continuation = new ContinuationImpl(var1) {
// $FF: synthetic field
Object result;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return ThirdActivity.this.getInfo(this);
}
};
}
Object $result = ((<undefinedtype>)$continuation).result;
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(((<undefinedtype>)$continuation).label) {
case 0:
ResultKt.throwOnFailure($result);
((<undefinedtype>)$continuation).label = 1;
if (DelayKt.delay(1000L, (Continuation)$continuation) == var4) {
return var4;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
Log.e("---", "延迟后执行的代码");
return "hello world!";
}
代码看着很乱,我们一步步分析:
- getInfo方法接收一个Continuation对象,已知这是一个回调接口,用于协程恢复的时候调用
- 将Continuation封装成了一个ContinuationImpl对象$continuation
,该对象还有一个标志位lable,初始等于0,并且有一个invokeSuspend()方法,该方法会调用getInfo方法,嗯?又调用回来了,看来这就是用来恢复代码用的! - 下面的代码竟然是一个switch case, 我们单独看一下这个switch case如下,他按照$continuation的lable值把代码分成了两部分,状态 =0 的时候,执行了挂起函数delay并且把 $continuation传给了delay方法,还把lable变成了1,然后直接return返回不再执行后面的代码了。那么,后面会发生什么呢,当delay方法运行完成以后,会调用 $continuation的invokeSuspend()方法,该方法又会调用getInfo方法恢复代码,但这时候 $continuation的lable值已经是1了,switch case会进入1的判断,继续执行后面的代码。
那么,整体流程就非常清晰了,我们通过状态机将代码分成了两部分(挂起前和挂起后),然后通过回调接口在挂起函数执行完毕以后回来再次执行原函数,但因为状态机的状态已经变了,我们只能执行挂起后的代码。
所以:挂起函数的本质就是 CallBack回调接口 + 状态机(switch case + 状态值 实现 代码分段执行)
我们再想一下,我们都把代码分段了,我们在代码分段的基础上增加一下线程切换,这不过分吧。
我们再想一下,挂起函数用的是回调接口,我们在回调接口的基础上增加嵌套或者并行,这不过分吧。
这思路、代码功能的实现不就扩展起来了吗!大家可以查看反编译更多kotlin协程提供给我们的方法,去验证各种思路。
可以看到,kotlin协程底层为我们做了非常多的事情,使我们开发者用最简单的方法实现复杂的功能,这也是kotlin协程的优势。
本文主要用来快速了解挂起函数的原理,肯定有讲的不合适的地方,希望大家多多指正。