Kotlin Primer·第七章·协程库(中篇)

7.5 kotlin 协程使用

首先回顾上一篇文章,你需要明白一点,协程是通过编码实现的一个任务。它和操作系统或者 JVM 没有任何关系,它的存在更类似于虚拟的线程。
以我在 qcon 上分享的一个例子来做介绍:

kotlin 协程

kotlin 的语法会让很多人觉得launch()async()是两个协程方法。其实不然,真正的协程是launch()传入的闭包参数。当launch()调用的时候,会启动一个协程(本质上并不一定是立即启动,下一篇文章解释)。
async()方法调用的时候又启动了一个协程,此刻外部协程的状态(包括CPU、方法调用、变量信息)会被暂存,进而切换到async()启动的协程执行。
在本例子中,launch()async()这两个方法都显式传入了两个参数:

1、第一个参数是一个协程的上下文,类型是CoroutineContext
CoroutineContext不仅可以用于在协程跳转的时刻传递数据,同时最主要的功能,也是在本例中的作用是用于表明协程运行与恢复时的上下文环境。
例如launch()方法中的UI参数,他实际上是一个封装了HandleCoroutineContext对象。

  • val UI = HandlerContext(Handler(Looper.getMainLooper()), "UI")

对应的还有Swing,当然在 Android 中是没有这个对象的,但在 Java 工程中是有的:

  • object Swing : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
  • override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
  • SwingContinuation(continuation)
  • }

2、第二个参数是一个lambda表达式,也就是协程体。kotlin 语法,当一个 lambda是函数的最后一个参数的时候,lambda可以写在圆括号外面。

7.6 suspend

一个协程方法(或闭包)必须被 suspend 修饰,同时 suspend 修饰的方法(或闭包)只能调用被suspend修饰过的方法(或闭包)。
suspend修饰后代码发生了怎样的变化?

我们知道,kotlin的闭包(lambda)在被编译后是转换成了内部类的对象,而一个被suspend修饰的闭包,就是一个特殊的内部类。例如这段例子:

  • fun test(){
  • launch {
  • val job = async {
  • "string"
  • }
  • println("========${job.await()}")
  • }
  • }

当他被编译以后,launch()传入的闭包会被编译成下面的样子:

  • final class Main$test$1 extends CoroutineImpl implements Function2<CoroutineScope, Continuation<? super Unit>, Object> {
  • public final Continuation<Unit> create(@NotNull CoroutineScope $receiver, @NotNull Continuation<? super Unit> continuation) {
  • }
  • public final Object invoke(@NotNull CoroutineScope $receiver, @NotNull Continuation<? super Unit> continuation) {
  • }
  • public final Object doResume(@Nullable Object obj, @Nullable Throwable th) {
  • }
  • }

而如果是一个普通方法被suspend修饰了以后,则只是会多出一个参数,例如一个普通的test()无参无内容方法用suspend修饰了以后会被编译成这样:

  • public final Object test(Continuation<? super Unit> continuation) {
  • return Unit.INSTANCE;
  • }

可以看到不论怎样,都会具备一个Continuation的对象。而这个Continuation就是真正的kotlin的协程。

7.7 协程挂起与恢复

理解了 suspend 做的事情以后,我们再来看 kotlin 的协程。
上面的代码中涉及到一个协程切换的情况。就是在launch()调用的时候,启动了一个协程就是suspend修饰的闭包参数。在launch()启动的协程内,async()又启动了一个协程。实际上协程的切换,就是一个挂起当前协程,启动新协程的过程。

7.7.1 协程的启动流程

挂起是指什么意思?首先要知道协程的启动流程。
launch()的源码是这样的:

  • public fun launch(
  • context: CoroutineContext = DefaultDispatcher,
  • start: CoroutineStart = CoroutineStart.DEFAULT,
  • block: suspend CoroutineScope.() -> Unit
  • ): Job {
  • //省略一些列的初始化后
  • start(block, coroutine, coroutine)
  • return coroutine
  • }

我们看到声明,start 是一个枚举对象,默认值是DEFAULT,这里实际上是调用了枚举的invoke()方法。
最终会调到createCoroutineUnchecked,这是一个扩展方法,他的声明如下:

  • public fun <R, T> (suspend R.() -> T).createCoroutineUnchecked(
  • receiver: R, completion: Continuation<T>
  • ): Continuation<Unit> =
  • if (this !is CoroutineImpl)
  • buildContinuationByInvokeCall(completion) {
  • (this as Function2<R, Continuation<T>, Any?>).invoke(receiver, completion)
  • }
  • else
  • (this.create(receiver, completion) as CoroutineImpl).facade

看到这段代码中,通过判断this是不是CoroutineImpl来做不同的操作。而this是什么,是一个有suspend修饰的闭包R.() -> T,也就是上面launch()的参数传入的闭包。

还记得suspend修饰的闭包在编译后会变成什么吗?刚好是一个CoroutineImpl类的对象。
因此这里是调用了闭包的create()方法,最终将闭包创建成了Continuation对象并返回。
这也就验证了前面我讲的:Continuation就是真正的kotlin的协程
最后在创建好协程对象后,又会调用协程Continuationresume()方法(代码在上面提到的扩展方法里,就不列出了)而协程的resume()方法又会调用回编译后suspend闭包转换成的那个类里面的doResume方法(后面讲协程恢复的时候还会讲这里)。
所以绕了一圈又回来了。

7.7.2 协程的挂起

明白了启动流程以后,再来看挂起就清晰多了。我们看到代码(删去了一些判空和初始化):

```java
public final Object doResume(Object obj, Throwable th) {
StringBuilder append;

top Created with Sketch.