kotlin之协程基础知识

1.什么是协程

从本质上来说,协程是一种轻量级的线程,不同协程之间的切换仅在编程语言的层面就可以实现。而线程之间的切换需要依靠操作系统的调度才能实现。
我们可以在单个线程中创建多个协程,协程支持挂起,挂起的同时又不会阻塞当前线程,这也是我们常说的非阻塞式挂起。
如何理解Android中的协程,我们来看ChatGPT给出的答案:

2.在Android中如何使用协程

由于协程并没有写入Android的内置API中,所以在Android中使用协程,我们需要在Android项目中app目录下的build.gradle.kts文件中加入以下依赖:

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

目前官方最新的协程依赖库已经更新到1.8.1-Beta,而笔者对于协程方面的知识介绍都是基于上面我们引入的1.7.3版本。

3.协程的几种创建方式

GlobalScope

GlobalScope是一个单例类,它实现了CoroutineScope接口。

public object GlobalScope : CoroutineScope {
    override val coroutineContext: CoroutineContext get() = EmptyCoroutineContext
}

launch()则是CoroutineScope的一个扩展函数,该函数的返回值类型则是一个Job,我们使用该函数来启动一个协程,如下代码示例:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
) : Job
fun main() {
    GlobalScope.launch {
        println("hello launch.")
    }
}

运行这段代码,会发现没有任何日志输出。这是因为,Global.launch()每次创建的都是一个顶层协程,当应用程序运行结束时也会跟着一起结束,协程中的代码还未来得及去执行。要解决这个问题也比较简单,我们让程序延迟一段时间来保证JVM的存活,如下代码示例:

fun main() {
    GlobalScope.launch {
       println("hello launch.")
    }
    Thread.sleep(100)
}

// 输出
hello launch.

这里我们使用Thread.sleep()方法让当前线程阻塞100毫秒,然后成功输出了launch()函数中的日志。

CoroutineScope

CoroutineScope是Kotin协程库中的一个顶层函数:

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())
internal class ContextScope(context: CoroutineContext) : CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    ...
}
public interface CoroutineScope {
    // 此范围的上下文。上下文由作用域封装,并用于实现作为作用域扩展的协程生成器。
    // 不建议出于任何目的在常规代码中访问此属性,除非出于高级用途访问 Job 实例。
    public val coroutineContext: CoroutineContext
}

这里我们把CoroutineScope()函数、ContextScope类、以及CoroutineScope接口的代码都贴出来了,希望大家不要弄混了,因为这里的函数名和接口名确实是相同的。
CoroutineScope()函数要求我们传入一个协程上下文的参数,并将该上下文对象保存在ContextScope类中然后返回。

ContextScope(if (context[Job] != null) context else context + Job())

创建ContextScope对象的时候,这里会有一个if条件的判断,就是我们上下文中是否包含了Job类型的对象,如果没有就会帮我们强行添加一个。这里涉及到了协程上下文对象的数据结构,关于这块的内容,我们后面的文章也会详细说明,这里就不展开介绍了。而两个上下文对象可以相加,是重载了 + 操作符,在操作符重载的文章里我们也已经详细介绍了。
其用法和GlobalScope是一致的,都是使用CoroutineScope的扩展函数launch()来启动一个协程,如下代码示例:

fun main() {
    CoroutineScope(Dispatchers.IO).launch {
        println("hello launch.")
    }
    Thread.sleep(100)
}

GlobalScope创建的是一个全局的协程作用域,而CoroutineScope()函数创建的是一个临时的协程作用域。所以在实际开发中我们更多的是使用CoroutineScope()函数,而不是GlobalScope这个单例类。

runBlocking

runBlocking同样也是Kotlin协程库中的一个顶层函数:

public actual fun <T> runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T {
    ... 
    val currentThread = Thread.currentThread()
    ...
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}

和上面介绍到的GlobalScope和CoroutineScope()不同的是,使用runBlocking来创建一个协程作用域,该方法会先阻塞当前线程,直到协程中的代码执行完成,才会释放当前线程,而且该方法拥有返回值。具体用法也比较简单:

fun main() {
    val result = runBlocking {
        "hello runBlocking"
    }
    println("$result")
}

// 输出
hello runBlocking.

那么runBlocking是如何阻塞当前线程的呢?我们可以看BlockingCoroutine中的joinBlocking方法:

fun joinBlocking(): T {
    ...
    while (true) {
          if (isCompleted) break
    }
    ...
    val state = this.state.unboxState()
    (state as? CompletedExceptionally)?.let { throw it.cause }
    return state as T
}

为了方便阅读这里对源码进行了一些简化。在这里我们可以看到,runBlocking函数是通过while循环来阻塞当前线程的,通过判断当前协程中的任务是否已经执行完成,来取消循环,从而释放当前线程。
所以在实际开发中我们一定要慎用它,特别是在主线程中,一不小心可能就会造成主线程阻塞,随之就喜提一个ANR。

MainScope

MainScope同样是一个顶层函数,该方法返回一个CoroutineScope对象,并将我们的代码逻辑指定在主线中去运行。

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

关于参数SupervisorJob + Dispatchers.Main我们会在后面的文章详细讲解,这里我们只需要了解MainScope()函数可以为我们快速的创建一个运行在主线程的协程作用域。使用起来也比较方便,如下代码示例:

MainScope().launch { println("MainScope called...") }

4.上下文调度器

上面我们已经介绍到协程是一种轻量级的线程,我们可以在单个线程中创建多个协程。但是这并不意味着我们永远不需要开启线程了,比如说Android中要求网络请求必须在子线程中进行,即使你开启了协程去执行网络请求,假如它是在主线程中的协程,那么程序依然会出错。
所以我们对协程的定义:“轻量级的线程”。这是在协程的创建和切换的维度去定义的,实际开发中我们依然需要结合线程来使用。关于在协程中切换线程Kotlin协程库中已经帮我们封装好了,使用起来也比较方便。
在Dispatchers类中,Kotlin协程库为我们提供了四种不同的协程上下文调度器,它们共同特点是都实现了CoroutineDispatcher这个抽象类:

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor
public actual object Dispatchers {
    public actual val Default: CoroutineDispatcher = DefaultScheduler

    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher

    public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
    
    public val IO: CoroutineDispatcher = DefaultIoScheduler

具体的区别如下:

类型区别
Default默认使用一种低并发的线程策略,适合低密度的任务计算,在子线程中执行代码
Main切换到主线中执行代码
Unconfined不切换线程,在当前线程中执行代码
IO使用较高并发的线程策略,适合高密度的任务计算,在子线程中执行代码

类型区别Default默认使用一种低并发的线程策略,适合低密度的任务计算,在子线程中执行代码Main切换到主线中执行代码Unconfined不切换线程,在当前线程中执行代码IO使用较高并发的线程策略,适合高密度的任务计算,在子线程中执行代码
熟悉了协程的上下文调度器,我们就可以在实际开发中灵活的运用它们了,如下代码示例:

CoroutineScope(Dispatchers.Main).launch {
    val useInfo = requestUserInfo()
    tvUserName.text = useInfo
    val homeInfo = requestHomeInfo()
    tvHomeInfo.text = homeInfo
}
suspend fun requestUserInfo() = withContext(Dispatchers.IO) {
    delay(1000)
    "requestUserInfo"
}

suspend fun requestHomeInfo() = withContext(Dispatchers.IO) {
    delay(1000)
    "requestHomeInfo"
}

在挂起函数withContext中,我们使用挂起函数delay来模拟网络请求中的耗时任务在子线程中去执行,获取到结果在切换到主线中更新UI。切换流程如下示意图:
在这里插入图片描述

5. withContext

withContext是一个挂起函数,关于挂起函数后面我们会详细的介绍。这里我们只需要知道使用suspend关键字修饰的函数就是挂起函数,挂起函数只能在协程作用域中或者在另一个挂起函数中访问。下面我们就来看下withContext函数的使用:

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T               
): T {
    return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
        ...
        val coroutine = DispatchedCoroutine(newContext, uCont)
        block.startCoroutineCancellable(coroutine, coroutine)
        coroutine.getResult()
    }
}

withContext函数接收两个参数,一个是协程上下文对象context,一个是带有接收者的函数类型参数block,而且这个函数类型是挂起函数类型。在该函数类型初始化的Lambda表达式中将会拥有当前协程的作用域。Lambda表达式中的最后一行代码也会作为withContext函数的返回值。withContext函数会将当前协程挂起,直到获取到结果后才会恢复当前协程。

fun main() {
    CoroutineScope(Dispatchers.IO).launch {
        val result = withContext<String>(coroutineContext) {
            return@withContext "result"
        }
        println(result)
    }
    Thread.sleep(100)
}

// 输出
result

这里我们使用父协程中的上下文对象作为参数传递给了withContext()函数,并且在Lambda表达式中返回了一个字符串result。在withContext函数内部又调用了suspendCoroutineUninterceptedOrReturn挂起函数,该函数会捕获到当前协程作用域中的Continuation实例,具体代码如下:

public suspend inline fun <T> suspendCoroutineUninterceptedOrReturn(crossinline block: (Continuation<T>) -> Any?): T {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    throw NotImplementedError("Implementation of suspendCoroutineUninterceptedOrReturn is intrinsic")
}

在这里我们并不能看到它是如何捕获到协程中的Continuation实例,我想这也是Kotlin协程设计Continuation的初衷,开发者只能通过suspendCoroutineUninterceptedOrReturn这个方法来获取,如果你对协程的使用比较熟悉,你会发现像suspendCoroutine()、suspendCancelableCoroutine()、delay()、yield()等方法中都是通过该方法来获取当前协程的Cotinuation实例。

6. async、await

async也是CoroutineScope的一个扩展函数,它总是和挂起函数await()成对的出现。该函数拥有一个Deferred类型的返回值:

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> 

当我们调用async函数时,其拥有协程作用域的Lambda表达式中的代码是立即执行的,而当我们调用await()函数时,如果async函数中的逻辑还没有执行完成,await()函数就会将当前协程挂起。下面我们就来看一个具体的案例:

fun main() {
    val curTimeMills = System.currentTimeMillis()
    runBlocking {
        val left = async { 5 + 5 }.await()
        val right = async { 6 + 6 }.await()
        println("result = ${left + right}")
    }
    val endTimeMills = System.currentTimeMillis() - curTimeMills
    println("time = $endTimeMills")
}

// 输出
result = 22
time = 262

在上面这段示例代码中我们可以看到,我们使用两个async()函数执行了一个简单的求和逻辑运算,并将最终的结果输出,总耗时是 262 毫秒。上面我们说了await()函数是会阻塞当前协程的,我们可以将上面的代码稍作改动:

fun main() {
    val curTimeMills = System.currentTimeMillis()
    runBlocking {
        val leftDef = async { 5 + 5 }
        val rightDef = async { 6 + 6 }
        println("result = ${leftDef.await() + rightDef.await()}")
    }
    val endTimeMills = System.currentTimeMillis() - curTimeMills
    println("time = $endTimeMills")
}

// 输出
result = 22
time = 186

最终我们的逻辑执行时长从 262 毫秒变成了 186 毫秒,当然这个例子还不够明显,如果aysnc函数中执行的是较耗时的任务,那么结果会更明显。我们再来分析一下这两段代码的区别:
1.第一种写法在调用完async函数后,立即执行了await()函数,那么后面的逻辑会等待async函数中的逻辑处理完成以后才会执行。
2.第二种写法开始先分别执行async()函数中的逻辑,然后再分别调用await()函数。这样我们两个async()函数在开始都可以执行,当我们调用await()方法的时候再去判断是否挂起当前协程。如果async()中的任务执行完成,则直接返回结果;如果未执行完成,则先挂起协程,等待任务执行完成。

7. coroutineScope

coroutineScope()函数同样也是一个挂起函数,与我们上面所介绍的CoroutineScope()函数并不是同一个函数,该函数首字母是小写的。它的特点是会继承外部的协程作用域并创建一个子作用域。同withContext函数一样两者都是使用Kotlin协程库中的顶层函数
suspendCoroutineUninterceptedOrReturn()函数来实现的。所以coroutineScope函数也会挂起当前协程,在任务执行完成后,再恢复当前协程。然后将获取到的结果在Lambda表达式中返回。

fun main() {
    runBlocking {
        val result = coroutineScope {
            println("coroutineScope called.")
            return@coroutineScope "coroutineScope"
        }
        println("result = $result")
    }
}

// 输出
coroutineScope called.
result = coroutineScope

从这段示例代码的输出结果来看,coroutineScope函数确实会先挂起当前协程,直到自己创建的子协程中的逻辑执行完成,才会释放当前协程。

8.挂起函数

1.挂起函数

Kotlin协程的挂起和恢复能力本质上就是挂起函数的挂起和恢复。在Kotlin中我们使用关键字suspend来声明一个挂起函数。
挂起函数只能在协程作用域内或其他挂起函数内调用。普通的函数无法调用挂起函数
如下代码示例,我们声明一个名为reqeust()的挂起函数:

suspend fun reqeust() { //doSomething }

2.挂起点

在协程内部挂起函数的调用处被称为挂起点,挂起点如果出现异步调用,那么当前协程就被挂起,直到对应的Continuation的resume函数被调用才会恢复当前协程。
例如,通过suspendCoroutine函数获得的Continuation是一个SafeContinuation的实例,与创建协程时得到的用来启动协程的Continuation实例没有本质上的差别。SafeContinuation类的作用也很简单,它可以确保只有发生异步调用时才会挂起,如下代码示例,虽然也有resume函数的调用,但协程并不会真的挂起。

suspend fun notSuspend() = suspendCoroutine<Int> { 
    it.resume(100)
}

异步调用是否发生,取决于resume函数与对应的挂起函数的调用是否在相同的调用栈上,切换函数调用栈的方法可以是切换到其他线程上执行,也可以是不切换线程但在当前函数返回之后的某一时刻再执行。前者比较容易理解,后者其实通常就是先将Continuation的实例保存下来,在后续合适的时机再调用。
这段描述来自霍丙乾老师的深入理解Kotlin协程。

3.CPS

挂起函数通过 Continuation-Passing-Style(CPS, 续体传递风格)实现。每个挂起函数都有一个附加的 Continuation [续体] 参数,在调用时隐式传入,这一过程是Kotlin编译器帮我们实现的,无需开发者自己动手实现。
那么Cotinuation是什么呢?事实上Cotinuation就是一个接口,它的具体源代码如下:

// 表示挂起点之后的延续接口。
public interface Continuation<in T> {
    // 与此延续相对应的协程上下文、
    public val context: CoroutineContext

    // 恢复执行相应的协程,传递一个成功或失败的 result 作为最后一个挂起点的返回值。
    public fun resumeWith(result: Result<T>)
}

关于这个接口,注释写的也比较清楚,context属性用来保存当前协程中的上下文对象,resumeWith方法用于恢复执行协程,并将执行结果作为最后一个挂起点的返回值。
例如我们有这么一个挂起函数request:

suspend fun <T> request() : T

在 CPS 变换之后,它的实现实际是这样的:

suspend fun request(completion: Cotinuation<T>): Any?

其返回类型 T 移动到了附加的续体参数的类型参数位置。
实现中的返回值类型 Any? 被设计用于表示挂起函数的动作。当挂起函数 挂起 协程时,函数返回一个特别的标识值 COROUTINE_SUSPENDED。如果一个挂起函数没有挂起协程,协程继续执行时,它直接返回一个结果或者抛出一个异常。这样,request() 函数实现中的返回值类型 Any? 实际上是 T 与 COROUTINE_SUSPENDED 的联合类型,这并不能在 Kotlin 的类型系统中表示出来。
而COROUTINE_SUSPENDED标志是个常量,定义在Intinsics.kt中

public val COROUTINE_SUSPENDED: Any get() = CoroutineSingletons.COROUTINE_SUSPENDED

internal enum class CoroutineSingletons { COROUTINE_SUSPENDED, UNDECIDED, RESUMED }

4.状态机

这里我们还是用上面所例举的例子来说明:

CoroutineScope(Dispatchers.Main).launch {
    // requestUserInfo()先挂起,返回COROUTINE_SUSPENDED,切换到子线程获取到结果
    // 再将获取到的结果传递给主线程
    val useInfo = requestUserInfo()
    tvUserName.text = useInfo
    
    // 同上requestUserInfo()的执行过程
    val homeInfo = requestHomeInfo()
    tvHomeInfo.text = homeInfo
}

这里涉及到我们requestUserInfo()函数内部withContext函数中创建的DispathcerCoroutine将会持有外部协程的Cotinuation实例,以及线程的切换流程,所以想要更清晰的看执行流程的同学可以把这段代码放到一个Activity中去,然后通过debug的方式来查看详细的执行流程。这里我们主要来介绍下有关这块代码涉及到的协程执行流程。
在Android Studio中依次打开Tools -> Kotlin -> Show Kotlin ByteCode我们就可以在右边的视图中看到其生成的字节码。

final class .../ContinuationKt$main$1 extends SuspendLambda implements Function2 { 
      public final invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;

      public final create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;

      public final invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
}

为了方便阅读这里将字节码做了一些简化。ContinuationKt$main$1熟悉字节码的同学应该不难看出这是一个内部类的命名方式,该类继承自SuspendLambda并实现了Kotlin中函数类型对应的接口Function2,也就是我们launch函数中所对应的挂起函数类型:
suspend CoroutineScope.() -> Unit的实例。
在Android Studio中我们双击shift在弹出的搜索框中,我们输入SuspendLambda,接着我们进入SuspendLambda类中我们可以看到其继承关系如下:

-- Continuation
   -- BaseContinuationImpl
      -- ContinuationImpl
         -- SuspendLambda

在BaseContinautionImpl这个抽象类中我们可以看到有一个名为invokeSuspend的抽象方法,并且我们在resumeWith方法中调用了该方法,invokeSuspend方法在协程状态流转中起着关键性的作用。下面是BaseContinautionImpl类的源码:

internal abstract class BaseContinuationImpl(
    // 启动该协程时创建的Cotinuation实例,对于launch函数是StandaloneCoroutine
    // 对于withContext是DisPatchedCoroutine
    public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
    
    public final override fun resumeWith(result: Result<Any?>) {
        var current = this
        var param = result
        while (true) {
            // 无需关注
            probeCoroutineResumed(current)
            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)
                    }
                releaseIntercepted() 
                if (completion is BaseContinuationImpl) {
                    current = completion
                    param = outcome
                } else {
                    // 恢复协程,传递结果值
                    completion.resumeWith(outcome)
                    return
                }
            }
        }
    }
    
protected abstract fun invokeSuspend(result: Result<Any?>): Any?

...
}

launch()函数反编译后的Java代码:

launch$default(CoroutineScope((CoroutineContext)Dispatchers.getMain()), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
   int label;

   public final Object invokeSuspend(Object $result) {
      Object result;
      label: {
         Object state = IntrinsicsKt.getCOROUTINE_SUSPENDED();
         switch (this.label) {
            case 0:
               // 检查结果
               ResultKt.throwOnFailure($result);
               this.label = 1;
               // 挂起,执行异步操作
               if (state == requestUserInfo(this))
               return state
               break;
               
            case 1:
               // 检查结果
               ResultKt.throwOnFailure($result);
               result = $result;
               break;
               
            case 2:
               // 检查结果
               ResultKt.throwOnFailure($result);
               result = $result;
               break label;
            default:
               throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
         }

         String requestUserInfo = (String)result;
         this.label = 2;
         // 挂起,执行异步操作
         if (state == requestHomeInfo(this)) 
         return state
      }

      String requestHomeInfo = (String)result;
      return Unit.INSTANCE;
   }

   public final Continuation create(Object value, 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);

为了方便阅读,这里将反编译的代码进行了一些调整。这里你可能会感到疑惑,invokeSuspend方法是否会多次调用,答案是一定的。每当withContext函数挂起后,切换到子线程执行异步任务获取结果后,都会将结果值传递给外层的Continuation实例,获取结果后再传递到主线程,然后进行下一个状态的流转,也就是通过我们上面的lable来控制的。代码可能不太好理解,这里笔者就结合我们上面这个示例用几张流程图来说明:
类的继承关系:
在这里插入图片描述

  • 1、处标记的地方是上下文调度器Dispatchers.Main、Dispatchers.Io。
  • 2、3、4处标记的地方是我们协程执行流程中所创建的Continuation[续体]的几种类型:
    其中2处是编译器为我们创建的,3处是我们在launch、withContext函数中创建的、4处是我们在ContinuationImpl中的intercepted方法中创建的。而协程启动过程中使用create方法创建的Continuation实例和我们标记的2处是同一类型的。

执行流程示意简图:
在这里插入图片描述

对于上述的代码示例来说,这是一个比较完整的协程执行流程闭环图。当然这里有很多细节省略了。

  • 1、2、3、4、5、6代表着Continuaiton实例,其中1、3、5是相同的类型:
    $name extends SuspendLambda implement Function2
    为什么intercepted方法会创建一个DispachedContinuation?下面我们就根据具体的源码来分析一下:
public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =
    (this as? ContinuationImpl)?.intercepted() ?: this
internal abstract class ContinuationImpl(
    completion: Continuation<Any?>?,
    private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {
    ...
    public fun intercepted(): Continuation<Any?> =
    intercepted?.(context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
    .also { intercepted = it }
   
}

这里的context[ContinuationInterceptor]中的context是一个CombinedContext,我们获得的结果是Dispacher.Main或Dispacher.IO。它们都是CoroutineDisptacher的子类,所以这里调用的是CoroutineDisptacher中覆盖的方法interceptContinuation,具体代码如下:

public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
    DispatchedContinuation(this, continuation)

该方法的返回值就是一个DispatchedContinuation实例。
上下文和续体流转示意简图:
在这里插入图片描述

总结:
首先我们先来回顾下这篇文章的主要内容:
1.通过单例类GlobleScope、顶层函数CoroutineScope、runBlocking、MainScope来创建并启动一个协程
2.挂起函数withContext、async/await、coroutineScope的运用
3.协程内部通过CPS + 状态机的方式来控制协程的执行流程
第一次写有关协程的文章。这篇文章可能也不是那么好理解,但是笔者的个人水平也有限。如有什么不对的地方,欢迎指出,大家一起学习。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值