协程挂起和恢复原理

131 阅读4分钟

一、如何开启一个原始的协程

1.1、协程流程分析

纵观几种主流的开启协程方式,它们最终都会调用到:

#CoroutineStart.kt
public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: 		Continuation<T>): Unit =
        when (this) {
            DEFAULT -> block.startCoroutineCancellable(receiver, completion)
            ATOMIC -> block.startCoroutine(receiver, completion)
            UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
            LAZY -> Unit // will start lazily
        }

无论走哪个分支,都是调用block的函数,而block 就是我们说的被suspend 修饰的函数。

  • 以DEFAULT 为例startCoroutineUndispatched
#Cancellable
public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>): Unit = runSafely(completion) {
   createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit))
}

接下来会调用到IntrinsicsJvm.kt里的:

#IntrinsicsJvm.kt
public actual fun <R, T> (suspend R.() -> T).createCoroutineUnintercepted(
    receiver: R,
    completion: Continuation<T>
)

该函数带了俩参数,其中的receiver 为接收者,而completion 为协程结束后调用的回调。 为了简单,我们可以省略掉receiver。 刚好IntrinsicsJvm.kt 里还有另一个函数:

#IntrinsicsJvm.kt
public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
    completion: Continuation<T>
): Continuation<Unit> 

createCoroutineUnintercepted 为 (suspend () -> T) 类型的扩展函数,因此只要我们的变量为 (suspend () -> T)类型就可以调用createCoroutineUnintercepted(xx)函数。

创建完成后,还需要开启协程函数:

#DispatchedContinuation
public fun <T> Continuation<T>.resumeCancellableWith(
    result: Result<T>,
    onCancellation: ((cause: Throwable) -> Unit)? = null
): Unit = when (this) {
    is DispatchedContinuation -> resumeCancellableWith(result, onCancellation)
    else -> resumeWith(result)
}
  • 以ATOMIC 为例startCoroutine
#Continuation
public fun <T> (suspend () -> T).startCoroutine(
    completion: Continuation<T>
) {
    createCoroutineUnintercepted(completion).intercepted().resume(Unit)
}

public inline fun <T> Continuation<T>.resume(value: T): Unit =
    resumeWith(Result.success(value))

和上面一样,也是会先创建协程createCoroutineUnintercepted,创建完成后再执行协程resume

由上可知:

  • 协程执行流程有3步,createCoroutineUnintercepted创建、intercepted拦截、resumeWith执行;
  • 所有启动方式中,suspendCoroutine<?>{}内部没有主动调用resumeWith,所以需要手动调用,协程才能往下执行。

ContinuationImpl继承BaseContinuationImpl,BaseContinuationImpl实现Continuation接口,BaseContinuationImpl有如下三个函数:create由子类实现、invokeSuspend由子类实现、resumeWith自己实现,内部会执行invokeSuspend

查看resumeWith源码:

#BaseContinuationImpl
public final override fun resumeWith(result: Result<Any?>) {
        var current = this
        var param = result
        while (true) {
            with(current) {
                val outcome: Result<Any?> =
                    try {
                        val outcome = invokeSuspend(param)
                        if (outcome === COROUTINE_SUSPENDED) return
                        Result.success(outcome)
                    } catch (exception: Throwable) {
                        Result.failure(exception)
                    }
                if (completion is BaseContinuationImpl) {
                    current = completion
                    param = outcome
                } else {
                    completion.resumeWith(outcome)
                    return
                }
            }
        }
    }
  • 如果返回值是挂起状态,则函数直接退出;
  • 如果不是,则调用resumeWith恢复调度,并将结果返回。

二、suspend 本质

suspend 的本质,就是 CallBack

suspend fun getUserInfo(): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "BoyCoder"
}

有的小伙伴要问了,哪来的 CallBack?明明没有啊。确实,我们写出来的代码没有 CallBack,但 Kotlin 的编译器检测到 suspend 关键字修饰的函数以后,会自动将挂起函数转换成带有 CallBack 的函数。

如果我们将上面的挂起函数反编译成 Java,结果会是这样:

//                              Continuation 等价于 CallBack
//                                         ↓         
public static final Object getUserInfo(Continuation $completion) {
  ...
  return "BoyCoder";
}

从反编译的结果来看,挂起函数确实变成了一个带有 CallBack 的函数,只是这个 CallBack 的真实名字叫 Continuation。毕竟,如果直接叫 CallBack 那就太 low,对吧?

我们看看 Continuation 在 Kotlin 中的定义:

public interface Continuation<in T> {
    public val context: CoroutineContext
//      相当于 onSuccess     结果   
//                 ↓         ↓
    public fun resumeWith(result: Result<T>)
}

对比着看看 CallBack 的定义:

interface CallBack {
    void onSuccess(String response);
}

从上面的定义我们能看到:Continuation 其实就是一个带有泛型参数的 CallBack,除此之外,还多了一个 CoroutineContext,它就是协程的上下文。对于熟悉 Android 开发的小伙伴来说,不就是 context 嘛!也没什么难以理解的,对吧?

以上这个从挂起函数转换成CallBack 函数的过程,被称为:CPS 转换(Continuation-Passing-Style Transformation)。

看,Kotlin 官方用 Continuation 而不用 CallBack 的原因出来了:Continuation 道出了它的实现原理。当然,为了理解挂起函数,我们用 CallBack 会更加的简明易懂。

这个转换看着简单,其中也藏着一些细节。

1.1、函数类型的变化

上面 CPS 转换过程中,函数的类型发生了变化:suspend ()->String 变成了 (Continuation)-> Any?

这意味着,如果你在 Java 访问一个 Kotlin 挂起函数getUserInfo(),在 Java 看到 getUserInfo() 的类型会是:(Continuation)-> Object。(接收 Continuation 为参数,返回值是 Object)

在这个 CPS 转换中,suspend () 变成 (Continuation) 我们前面已经解释了,不难。那么函数的返回值为什么会从:String变成Any?

1.2、挂起函数的返回值

挂起函数经过 CPS 转换后,它的返回值有一个重要作用:标志该挂起函数有没有被挂起

这听起来有点绕:挂起函数,就是可以被挂起的函数,它还能不被挂起吗?是的,挂起函数也能不被挂起。

让我们来理清几个概念:

只要有 suspend 修饰的函数,它就是挂起函数,比如我们前面的例子:

suspend fun getUserInfo(): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "BoyCoder"
}

当 getUserInfo() 执行到 withContext的时候,就会返回 CoroutineSingletons.COROUTINE_SUSPENDED 表示函数被挂起了。

现在问题来了,请问下面这个函数是挂起函数吗:

// suspend 修饰
// ↓
suspend fun noSuspendFriendList(user: String): String{
    // 函数体跟普通函数一样
    return "Tom, Jack"
}

答案:它是挂起函数。但它跟一般的挂起函数有个区别:它在执行的时候,并不会被挂起,因为它就是普通函数。当你写出这样的代码后,IDE 也会提示你,suspend 是多余的:

image.png

noSuspendFriendList() 被调用的时候,不会挂起,它会直接返回 String 类型:"no suspend"。这样的挂起函数,你可以把它看作伪挂起函数

1.3、返回类型是 Any?的原因

由于 suspend 修饰的函数,既可能返回 CoroutineSingletons.COROUTINE_SUSPENDED,也可能返回实际结果"no suspend",甚至可能返回 null,为了适配所有的可能性,CPS 转换后的函数返回值类型就只能是 Any?了。

三、suspend 解密

假设一个简单的需求,先从网络获取最新的消息,再与本地数据库做增量操作,获取完整消息。 在原生中,回调代码如下:

fun fetch(){
        fetchRemote { msg->
            fetchLocal(msg) { result ->
                //实际业务操作
                println("result:$result")
            }
        }
}
 
fun fetchRemote(onNext:(Int)->Unit){
    Thread.sleep(300)
    val value = 1
 
    onNext(value)
}
fun fetchLocal(id:Int,onNext:(Int)->Unit){
    Thread.sleep(300)
    val value = 2
 
    onNext(id + value)
}

利用了kotlin协程,可以直接以同步方式:

fun fetch() {
    GlobalScope.launch {
        println("parent coroutine running")

        val msg = fetchRemote()

        val result = fetchLocal(msg)

        println("after suspend, result:$result")
    }
}

suspend fun fetchRemote(): Int = withContext(Dispatchers.IO) {
    println("son coroutine running  fetchRemote")
    100
}

suspend fun fetchLocal(msg: Int): Int = withContext(Dispatchers.IO) {
    println("son coroutine running  fetchLocal")
    msg + 1
}

上面的 fetch函数写法,就是传说中的 “同步代码实现异步操作” 了,简称「代码同步化」。

2.1、函数解体

Android Studio打开这段代码的Kotlin Bytecode。可以在Tools -> Kotlin -> Show Kotlin Bytecode中打开。

然后点击其中的Decompile选项,生成对应的反编译java代码。最终代码如下:

public final void fetch() {
  BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
	 int label;

	 @Nullable
	 public final Object invokeSuspend(@NotNull Object $result) {
		Object var10000;
		label17: {
		   Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
		   Test var7;
		   switch(this.label) {
		   case 0:
			  ResultKt.throwOnFailure($result);
			  String var2 = "parent coroutine running";
			  System.out.println(var2);
			  var7 = Test.this;
			  this.label = 1;
			  var10000 = var7.fetchRemote(this);
			  if (var10000 == var5) {
				 return var5;
			  }
			  break;
		   case 1:
			  ResultKt.throwOnFailure($result);
			  var10000 = $result;
			  break;
		   case 2:
			  ResultKt.throwOnFailure($result);
			  var10000 = $result;
			  break label17;
		   default:
			  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
		   }

		   int msg = ((Number)var10000).intValue();
		   var7 = Test.this;
		   this.label = 2;
		   var10000 = var7.fetchLocal(msg, this);
		   if (var10000 == var5) {
			  return var5;
		   }
		}

		int result = ((Number)var10000).intValue();
		String var4 = "after suspend, result:" + result;
		System.out.println(var4);
		return Unit.INSTANCE;
	 }

	 @NotNull
	 public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
		Intrinsics.checkNotNullParameter(completion, "completion");
		Function2 var3 = new <anonymous constructor>(completion);
		return var3;
	 }

	 public final Object invoke(Object var1, Object var2) {
		return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
	 }
  }), 3, (Object)null);
}

@Nullable
public final Object fetchRemote(@NotNull Continuation $completion) {
  return BuildersKt.withContext((CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
	 int label;

	 @Nullable
	 public final Object invokeSuspend(@NotNull Object var1) {
		Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
		switch(this.label) {
		case 0:
		   ResultKt.throwOnFailure(var1);
		   String var2 = "son coroutine running  fetchRemote";
		   System.out.println(var2);
		   return Boxing.boxInt(100);
		default:
		   throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
		}
	 }

	 @NotNull
	 public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
		Intrinsics.checkNotNullParameter(completion, "completion");
		Function2 var3 = new <anonymous constructor>(completion);
		return var3;
	 }

	 public final Object invoke(Object var1, Object var2) {
		return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
	 }
  }), $completion);
}

@Nullable
public final Object fetchLocal(final int msg, @NotNull Continuation $completion) {
  return BuildersKt.withContext((CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
	 int label;

	 @Nullable
	 public final Object invokeSuspend(@NotNull Object var1) {
		Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
		switch(this.label) {
		case 0:
		   ResultKt.throwOnFailure(var1);
		   String var2 = "son coroutine running  fetchLocal";
		   System.out.println(var2);
		   return Boxing.boxInt(msg + 1);
		default:
		   throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
		}
	 }

	 @NotNull
	 public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
		Intrinsics.checkNotNullParameter(completion, "completion");
		Function2 var3 = new <anonymous constructor>(completion);
		return var3;
	 }

	 public final Object invoke(Object var1, Object var2) {
		return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
	 }
  }), $completion);
}

2.2、执行流程

  • 每个suspend函数都会自动生成invokeSuspend()方法,该方法内部通过label标志维护了一套状态机机制;
  • launch启动协程后,会触发父协程执行resumeWith(),查看BaseContinuationImpl#resumeWith()源码可知,会执行父协程invokeSuspend()
  • 此时label为0,执行case=0的逻辑,执行到第一个挂起函数fetchRemote(),并且将label+1
  • fetchRemote() 是个普通函数,类似suspend fun a()=1这种只是简单声明suspend的函数,会直接返回函数结果值;
  • fetchRemote() 是实现了withContext的正经挂起函数时,函数会返回一个挂起标志CoroutineSingletons.COROUTINE_SUSPENDED,这也是会什么suspend函数返回值是Any类型;到这里父协程的resumeWith()中,outcome === COROUTINE_SUSPENDED为true,父协程挂起;
  • 对于fetchRemote(),当执行resumeWith()时,也会执行自身内部invokeSuspend(),此时自身内部label=0,执行case=0的逻辑,协程体内没有挂起函数,只有一个打印,所以不会返回挂起,而是执行BaseContinuationImpl#resumeWith()中的completion.resumeWith(outcome)恢复父协程;
  • completion.resumeWith(outcome)恢复执行父协程的invokeSuspend(),因为执行已经执行过一次,此时label=1,执行case=1的逻辑,恢复之前挂起的现场,然后进行break掉这个switch,往下执行fetchLocal()
  • 对于fetchLocal(),也和上述步骤一样,将父协程的label+1,然后返回挂起标志,等待执行自身resumeWith()invokeSuspend()恢复父协程;
  • 此时label=2,恢复之前挂起的现场,然后进行break掉整个状态机,然后往下执行非挂起代码。