Kotlin 协程源码解析(七)`delay()` 到底是如何在 `Dispatchers.Main` 上恢复的?

Kotlin 协程源码解析(七)delay() 到底是如何在 Dispatchers.Main 上恢复的?

在上一篇文章中,我们解决了 delay() 的"挂起"问题。

我们一路追踪了:

scss 复制代码
delay()
    ↓
suspendCancellableCoroutine
    ↓
获取 CancellableContinuation
    ↓
cont.context.delay
    ↓
scheduleResumeAfterDelay()
    ↓
COROUTINE_SUSPENDED

于是当前协程停止执行。

但是,我们还留下了最关键的一段:

scss 复制代码
scheduleResumeAfterDelay()
        ↓
      ???
        ↓
    continuation.resume()

上一篇文章最后留下的问题就是:

1000ms 到了以后,到底是谁调用了 Continuation 的 resume()

如果代码运行在 Android:

scss 复制代码
launch(Dispatchers.Main) {
    println("A")

    delay(1000)

    println("B")
}

那么我们今天要追踪的完整链路是:

scss 复制代码
delay(1000)
    ↓
scheduleResumeAfterDelay()
    ↓
Handler.postDelayed()
    ↓
Android MessageQueue
    ↓
1000ms 到期
    ↓
Looper 取出 Runnable
    ↓
Runnable.run()
    ↓
continuation.resumeUndispatched(Unit)
    ↓
协程恢复
    ↓
println("B")

除此之外,我们还要回答另一个很有意思的问题:

Dispatchers.Main 明明定义在 kotlinx.coroutines 中,它为什么知道 Android 的主线程在哪里?

答案是:

markdown 复制代码
kotlinx-coroutines-core
        ↓
MainDispatcherLoader
        ↓
MainDispatcherFactory
        ↑
        │
kotlinx-coroutines-android
        ↓
AndroidDispatcherFactory
        ↓
Looper.getMainLooper()
        ↓
Handler
        ↓
HandlerContext

这篇文章我们就从 scheduleResumeAfterDelay() 开始。


一、重新回到上一篇留下的代码

先回忆一下 delay()

kotlin 复制代码
public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return

    return suspendCancellableCoroutine { cont ->
        cont.context.delay
            .scheduleResumeAfterDelay(timeMillis, cont)
    }
}

上一篇我们重点关注的是:

markdown 复制代码
suspendCancellableCoroutine { cont ->
    ...
}

它拿到了当前协程的:

复制代码
CancellableContinuation

然后:

arduino 复制代码
cont.context.delay

找到了当前 CoroutineContext 对应的 Delay

这里有一个非常值得注意的细节。

当前版本的源码中:

csharp 复制代码
internal val CoroutineContext.delay: Delay
    get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay

也就是说,delay 能力实际上是从 CoroutineContext 中的 ContinuationInterceptor 获取的;而 CoroutineDispatcher 本身就是 ContinuationInterceptor 的实现。

因此可以把它粗略理解成:

markdown 复制代码
CoroutineContext
       ↓
ContinuationInterceptor
       ↓
CoroutineDispatcher
       ↓
是否同时实现 Delay?

如果实现了:

复制代码
使用 Dispatcher 自己的 Delay

否则:

复制代码
使用 DefaultDelay

这也是为什么:

scss 复制代码
launch(Dispatchers.Main) {
    delay(1000)
}

和:

scss 复制代码
launch(Dispatchers.Default) {
    delay(1000)
}

最终可以使用不同的延迟机制。


二、Android 中的 Dispatchers.Main 究竟是什么?

我们平时写:

复制代码
Dispatchers.Main

很容易形成一个简单的认识:

Dispatchers.Main 就是 Android 主线程。

这个认识作为日常开发的抽象当然没有问题。

但从源码角度看,它其实是一个对象。

而且:

复制代码
Dispatchers.Main

并不是简单地:

scss 复制代码
Handler(Looper.getMainLooper())

直接写死在 kotlinx.coroutines-core 里面。

因为 kotlinx.coroutines 是跨平台库。

例如:

markdown 复制代码
Android
    → Handler + Looper

JavaFX
    → JavaFX Application Thread

Swing
    → Swing Event Dispatch Thread

Darwin
    → Main Queue

如果 core 模块直接写死 Android API,那么整个协程库就不可能做到这样的跨平台设计。

所以 Kotlin 协程采用了一个非常漂亮的设计:

css 复制代码
core
    ↓
我需要一个 Main Dispatcher
    ↓
但我不知道平台是什么
    ↓
让具体平台自己提供实现

于是就出现了:

复制代码
MainDispatcherFactory

三、MainDispatcherFactory:让平台提供自己的 Main Dispatcher

可以把这个接口简化成:

kotlin 复制代码
interface MainDispatcherFactory {

    fun createDispatcher(
        allFactories: List<MainDispatcherFactory>
    ): MainCoroutineDispatcher

    val loadPriority: Int
}

它表达的意思其实非常简单:

不同平台可以提供自己的 Main Dispatcher 创建工厂。

例如:

markdown 复制代码
Android
    ↓
AndroidDispatcherFactory

JavaFX
    ↓
JavaFxDispatcherFactory

Swing
    ↓
SwingDispatcherFactory

这样 kotlinx.coroutines-core 就不需要知道:

复制代码
Android
Handler
Looper

这些东西。

它只需要知道:

复制代码
MainDispatcherFactory

四、Android 是怎么把自己的 Factory 提供给协程库的?

假设我们的 Android 项目加入:

scss 复制代码
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:...")

这时候,Android 模块就进入了应用。

这个模块里面提供了:

kotlin 复制代码
internal class AndroidDispatcherFactory : MainDispatcherFactory

也就是说:

css 复制代码
kotlinx-coroutines-core
        │
        │ 需要 Main Dispatcher
        ▼
MainDispatcherLoader
        │
        │ 查找 MainDispatcherFactory
        ▼
AndroidDispatcherFactory

当前 JVM 版本的 MainDispatcherLoader 会加载所有可用的 MainDispatcherFactory,然后选择 loadPriority 最高的 Factory 来创建 Main Dispatcher;在 Android 环境中,kotlinx-coroutines-android 提供的 AndroidDispatcherFactory 就参与了这个过程。

这里底层使用的是 JVM 的:

复制代码
ServiceLoader

以及 Kotlin 协程为了性能提供的:

复制代码
FastServiceLoader

所以我们可以先把它理解成:

协程库启动时,会去寻找当前运行环境提供的 Main Dispatcher 实现。

这就解释了一个非常有意思的问题:

复制代码
Dispatchers.Main

为什么不需要我们自己告诉它:

复制代码
"我是 Android,请使用 Android 主线程。"

因为:

复制代码
AndroidDispatcherFactory

已经通过平台模块提供给它了。


五、真正创建 Dispatchers.Main

找到:

复制代码
AndroidDispatcherFactory

以后,就会调用:

scss 复制代码
createDispatcher()

Android 的实现非常简单:

kotlin 复制代码
internal class AndroidDispatcherFactory : MainDispatcherFactory {

    override fun createDispatcher(
        allFactories: List<MainDispatcherFactory>
    ) =
        HandlerContext(
            Looper.getMainLooper().asHandler(async = true)
        )
}

这里终于出现了我们熟悉的:

scss 复制代码
Looper.getMainLooper()

它拿到 Android 的:

css 复制代码
Main Looper

然后:

css 复制代码
Main Looper
    ↓
Handler
    ↓
HandlerContext

最终得到:

复制代码
Dispatchers.Main

所以我们终于可以把:

复制代码
Dispatchers.Main

展开成:

markdown 复制代码
Dispatchers.Main
        ↓
MainDispatcherLoader
        ↓
AndroidDispatcherFactory
        ↓
HandlerContext
        ↓
Handler
        ↓
Looper.getMainLooper()

Android 的 AndroidDispatcherFactory 当前源码就是通过 Looper.getMainLooper() 创建 Handler,再构造 HandlerContext


六、HandlerContext 到底是什么?

现在我们已经找到:

markdown 复制代码
Dispatchers.Main
    ↓
HandlerContext

接下来只需要看看 HandlerContext

它可以理解成:

把 Android Handler 包装成 Kotlin 协程能够理解的 Dispatcher。

它同时具备两个能力:

markdown 复制代码
HandlerContext
    │
    ├── CoroutineDispatcher
    │
    └── Delay

这正是我们今天最关心的地方。

上一篇我们说:

scss 复制代码
Delay
    ↓
scheduleResumeAfterDelay()

那么现在问题变得非常具体:

HandlerContextscheduleResumeAfterDelay() 是怎么实现的?

答案非常简单。


七、scheduleResumeAfterDelay() 的真正实现

HandlerContext 实现了:

kotlin 复制代码
override fun scheduleResumeAfterDelay(
    timeMillis: Long,
    continuation: CancellableContinuation<Unit>
) {
    val block = Runnable {
        with(continuation) {
            resumeUndispatched(Unit)
        }
    }

    handler.postDelayed(
        block,
        timeMillis.coerceAtMost(MAX_DELAY)
    )

    continuation.invokeOnCancellation {
        handler.removeCallbacks(block)
    }
}

这里我们终于找到了上一篇一直没有展开的:

scss 复制代码
scheduleResumeAfterDelay()

它实际上做了两件事情。

第一件:

scss 复制代码
handler.postDelayed(block, timeMillis)

第二件:

kotlin 复制代码
continuation.resumeUndispatched(Unit)

所以:

scss 复制代码
scheduleResumeAfterDelay()
        ↓
Handler.postDelayed()

而:

scss 复制代码
1000ms 到期
        ↓
Runnable.run()
        ↓
resumeUndispatched(Unit)

到这里,delay() 的恢复机制终于浮出水面。

Android HandlerContext 的实现正是通过 Handler.postDelayed() 注册延迟任务,并在 Runnable 执行时调用 Continuation 的 resumeUndispatched(Unit)


八、postDelayed() 到底意味着什么?

现在我们暂时跳出协程。

看 Android 最原始的代码:

go 复制代码
handler.postDelayed({
    println("Hello")
}, 1000)

它的含义是:

把这个 Runnable 放到 Handler 对应 Looper 的消息队列中,并要求至少延迟指定时间后再执行。

这里最重要的是:

markdown 复制代码
Handler
    ↓
Looper
    ↓
MessageQueue

而 Handler 绑定的是:

css 复制代码
Main Looper

所以:

scss 复制代码
handler.postDelayed(block, 1000)

实际上就是把:

scss 复制代码
block

交给:

复制代码
Android 主线程的消息系统

等待 1000ms。


九、这时候 Android 系统到底做了什么?

可以把过程简化成:

scss 复制代码
handler.postDelayed(block, 1000)
        ↓
MessageQueue
        ↓
记录 block 的执行时间
        ↓
Looper.loop()
        ↓
等待消息

注意:

等待的是 Looper,不是我们的协程。

这点非常重要。

我们的协程在执行:

scss 复制代码
delay(1000)

的时候已经:

复制代码
挂起

了。

而 Main Thread 并没有因为这个协程而:

bash 复制代码
Thread.sleep(1000)

真正进入等待的是 Android 的消息循环。

所以可以把整个过程画成:

scss 复制代码
协程
    │
    │ delay()
    ▼
挂起
    │
    │
    │      Main Thread
    │          │
    │          ▼
    │    MessageQueue
    │          │
    │       等待时间
    │          │
    │          ▼
    │      Runnable
    │
    │
    └──────────┘

这就是:

协程挂起和线程等待是两个完全不同层次的事情。


十、1000ms 到了以后发生了什么?

这是今天最关键的一刻。

假设:

scss 复制代码
launch(Dispatchers.Main) {
    println("A")

    delay(1000)

    println("B")
}

执行到:

scss 复制代码
delay(1000)

以后:

scss 复制代码
协程
    ↓
suspendCancellableCoroutine
    ↓
scheduleResumeAfterDelay()
    ↓
Handler.postDelayed()
    ↓
COROUTINE_SUSPENDED

协程挂起。

与此同时:

复制代码
Runnable

已经被放入:

css 复制代码
Main Looper

对应的:

复制代码
MessageQueue

里面。

1000ms 后:

markdown 复制代码
MessageQueue
    ↓
取出 Runnable
    ↓
Looper
    ↓
执行 Runnable.run()

而 Runnable 的内容就是:

scss 复制代码
with(continuation) {
    resumeUndispatched(Unit)
}

所以:

scss 复制代码
Runnable.run()
        ↓
continuation.resumeUndispatched(Unit)

终于来了。


十一、resumeUndispatched() 又是什么?

看到这里可能会产生一个疑问:

上一篇我们一直说:

scss 复制代码
resume()
    ↓
resumeWith()

为什么这里突然变成了:

scss 复制代码
resumeUndispatched()

这正好是后面 Dispatcher 章节要深入研究的内容。

现在我们只需要理解它的名字:

lua 复制代码
resume

表示:

恢复 Continuation。

而:

复制代码
resumeUndispatched

可以先粗略理解成:

当前已经在合适的执行线程上,因此不需要再经过一次 Dispatcher 调度。

注意这个前提:

复制代码
Runnable

本身就是由:

css 复制代码
Main Looper

执行的。

所以:

scss 复制代码
Runnable.run()

发生的时候,我们已经在:

css 复制代码
Main Thread

了。

这时候如果又把恢复任务:

复制代码
dispatch

一次 Main Dispatcher,就有点多此一举。

因此这里直接:

kotlin 复制代码
continuation.resumeUndispatched(Unit)

恢复。

这一点我们先记住。

为什么可以直接恢复?

因为:

scss 复制代码
Handler.postDelayed()

已经保证 Runnable 会在 Main Looper 对应的线程执行。

至于 resumeUndispatched() 内部到底如何避免再次 Dispatch,以及它和我们之前分析过的 DispatchedContinuation 是什么关系,放到后面的 Dispatcher 章节再完整展开。


十二、恢复以后,状态机从哪里开始?

现在把上一篇的内容重新接起来。

我们最初执行:

scss 复制代码
launch(Dispatchers.Main) {
    println("A")

    delay(1000)

    println("B")
}

第一次执行:

scss 复制代码
println("A")
        ↓
delay()
        ↓
suspendCancellableCoroutine
        ↓
scheduleResumeAfterDelay()
        ↓
Handler.postDelayed()
        ↓
COROUTINE_SUSPENDED

于是当前执行结束。

1000ms 后:

scss 复制代码
Handler
    ↓
Runnable.run()
    ↓
continuation.resumeUndispatched(Unit)

Continuation 被恢复。

然后:

scss 复制代码
resume
    ↓
状态机
    ↓
invokeSuspend()
    ↓
根据 label 找到下一个状态
    ↓
println("B")

所以完整过程终于可以画成:

scss 复制代码
第一次执行

launch
  │
  ▼
println("A")
  │
  ▼
delay()
  │
  ▼
suspendCancellableCoroutine
  │
  ▼
scheduleResumeAfterDelay()
  │
  ▼
Handler.postDelayed()
  │
  ▼
COROUTINE_SUSPENDED
  │
  ▼
当前执行结束


1000ms 后

Android MessageQueue
  │
  ▼
Looper
  │
  ▼
Runnable.run()
  │
  ▼
continuation.resumeUndispatched(Unit)
  │
  ▼
invokeSuspend()
  │
  ▼
状态机恢复
  │
  ▼
println("B")

至此:

delay() 的挂起和恢复终于完整闭环了。


十三、这里最值得注意的是:恢复者其实不是 delay()

现在回头看整个过程。

delay() 做的是:

markdown 复制代码
获取 Continuation
        ↓
交给 Delay
        ↓
安排未来恢复

Android Delay 做的是:

scss 复制代码
Handler.postDelayed()

Android 系统做的是:

复制代码
到时间以后执行 Runnable

Runnable 做的是:

kotlin 复制代码
continuation.resumeUndispatched(Unit)

所以严格来说:

scss 复制代码
delay()

并没有自己恢复协程。

甚至:

复制代码
Handler

也没有直接恢复协程。

真正发生的是:

markdown 复制代码
Android
    ↓
执行 Runnable
    ↓
Runnable
    ↓
调用 Continuation
    ↓
Continuation 恢复状态机

所以可以把责任划分成:

markdown 复制代码
delay
    │
    └── 提出需求:
        "1000ms 后恢复我"

Delay
    │
    └── 安排:
        "我会负责注册这个延迟任务"

Handler
    │
    └── 注册:
        "把 Runnable 放进 MessageQueue"

Looper
    │
    └── 到时间:
        "执行 Runnable"

Runnable
    │
    └── 恢复:
        "Continuation,继续执行吧"

Continuation
    │
    └── 驱动:
        "状态机继续执行"

这个职责划分其实非常漂亮。


十四、为什么 Dispatchers.Main 同时能够"调度"和"延迟"?

现在再看:

复制代码
HandlerContext

你会发现一个非常有意思的事情。

它同时实现了:

复制代码
CoroutineDispatcher

和:

复制代码
Delay

也就是说,同一个对象同时提供了两种能力:

scss 复制代码
HandlerContext
       │
       ├───────────────┐
       │               │
       ▼               ▼
CoroutineDispatcher    Delay
       │               │
       ▼               ▼
   handler.post()   handler.postDelayed()

这两个 API 虽然都依赖:

复制代码
Handler

但是承担的职责不同。

dispatch()

解决:

现在要执行一个协程任务,把它交给谁?

于是:

scss 复制代码
dispatch()
    ↓
handler.post()

scheduleResumeAfterDelay()

解决:

过一段时间以后,要恢复一个已经挂起的协程。

于是:

scss 复制代码
scheduleResumeAfterDelay()
    ↓
handler.postDelayed()

所以可以把它们简单理解成:

markdown 复制代码
HandlerContext

    立即调度
        ↓
    Handler.post()

    延迟恢复
        ↓
    Handler.postDelayed()

这里我们暂时不深入 dispatch() 的实现。

因为接下来我们会专门研究:

Dispatcher 到底是怎么决定一个协程应该在哪个线程执行的?

那才是 Dispatcher 真正值得研究的地方。


十五、取消又是怎么处理的?

这里还有一个非常重要但容易被忽略的细节。

假设:

scss 复制代码
launch(Dispatchers.Main) {
    delay(10000)

    println("B")
}

执行到:

scss 复制代码
delay(10000)

之后,协程挂起。

同时:

scss 复制代码
Handler.postDelayed(block, 10000)

已经注册。

但是:

scss 复制代码
job.cancel()

发生了。

这时候显然不能让这个延迟任务永远留在 MessageQueue 中。

所以 HandlerContext 会把:

复制代码
Continuation

的取消和:

复制代码
Handler

上的任务关联起来。

大致可以理解为:

markdown 复制代码
Coroutine Cancellation
        ↓
取消 Continuation
        ↓
移除 Handler 中的 Runnable

所以:

scss 复制代码
delay
    │
    ├── 正常情况
    │      ↓
    │   postDelayed()
    │      ↓
    │   Runnable
    │      ↓
    │   resume
    │
    └── 取消情况
           ↓
        removeCallbacks()

这也是 suspendCancellableCoroutine 比简单的 suspendCoroutine 更适合这种场景的重要原因之一。

不过取消和 Continuation 的状态竞争本身是一个很大的话题,我们之后讲协程取消与异常时再详细展开。


十六、现在把整个链路再串一次

现在我们终于可以回答最开始的问题:

delay(1000) 到底是怎么在 Android 主线程上恢复的?

完整答案是:

scss 复制代码
launch(Dispatchers.Main)
        │
        ▼
    Dispatchers.Main
        │
        ▼
   HandlerContext
        │
        ├───────────────┐
        │               │
        ▼               ▼
CoroutineDispatcher     Delay
                        │
                        ▼
                 scheduleResumeAfterDelay()
                        │
                        ▼
                 Handler.postDelayed()
                        │
                        ▼
                  MessageQueue
                        │
                     1000ms
                        │
                        ▼
                     Looper
                        │
                        ▼
                  Runnable.run()
                        │
                        ▼
          continuation.resumeUndispatched(Unit)
                        │
                        ▼
                  invokeSuspend()
                        │
                        ▼
                    状态机
                        │
                        ▼
                  println("B")

而:

复制代码
Dispatchers.Main

是怎么来的?

markdown 复制代码
kotlinx-coroutines-core
        │
        ▼
MainDispatcherLoader
        │
        ▼
MainDispatcherFactory
        ▲
        │
kotlinx-coroutines-android
        │
        ▼
AndroidDispatcherFactory
        │
        ▼
Looper.getMainLooper()
        │
        ▼
Handler
        │
        ▼
HandlerContext
        │
        ▼
Dispatchers.Main

这两条链最终汇合在一起:

scss 复制代码
                  Dispatchers.Main
                         │
                         ▼
                   HandlerContext
                    /          \
                   /            \
                  ▼              ▼
        CoroutineDispatcher     Delay
               │                  │
               ▼                  ▼
         Handler.post()    Handler.postDelayed()
                                  │
                                  ▼
                             MessageQueue
                                  │
                                  ▼
                               Looper
                                  │
                                  ▼
                              Runnable
                                  │
                                  ▼
                         Continuation.resume

十七、这一篇真正应该记住什么?

如果只记住几个结论,我认为最重要的是下面几个。

第一,Dispatchers.Main 不是"一个线程"

更准确地说:

css 复制代码
Dispatchers.Main
    ↓
MainCoroutineDispatcher
    ↓
Android HandlerContext
    ↓
Handler
    ↓
Main Looper

它是一套:

让协程能够运行在 Android 主线程上的调度实现。


第二,Android 并不是把 Dispatchers.Main 写死在 core

而是:

markdown 复制代码
core
    ↓
MainDispatcherFactory

平台模块提供:

复制代码
AndroidDispatcherFactory

然后通过:

复制代码
MainDispatcherLoader

找到它。

这也是为什么:

复制代码
kotlinx-coroutines-core

能够保持平台无关。

而:

复制代码
kotlinx-coroutines-android

负责提供 Android 的具体实现。


第三,HandlerContext 同时实现了 Dispatcher 和 Delay

所以:

scss 复制代码
dispatch()
    ↓
Handler.post()

负责把任务调度到 Handler 所在线程。

而:

scss 复制代码
scheduleResumeAfterDelay()
    ↓
Handler.postDelayed()

负责安排未来恢复。

虽然这两个机制都使用 Handler,但它们解决的是不同的问题。


第四,真正恢复协程的是 Continuation

Android 到时间以后做的事情其实非常简单:

scss 复制代码
Looper
    ↓
执行 Runnable
    ↓
resumeUndispatched(Unit)

真正让状态机继续向下执行的,仍然是:

复制代码
Continuation

所以我们之前研究的:

scss 复制代码
Continuation
    ↓
resumeWith()
    ↓
invokeSuspend()
    ↓
状态机

在这里再次出现了。

这也正好把前面几篇文章的内容连接起来了。


十八、我们终于可以回答最开始的那个问题

还记得第一篇文章里留下的问题吗?

一个:

bash 复制代码
suspend

函数为什么能够:

复制代码
停下来

然后:

复制代码
过了一段时间

又:

复制代码
从原来的位置继续执行?

现在我们已经可以完整回答其中的 delay() 场景:

scss 复制代码
suspend 函数
    ↓
编译器生成状态机
    ↓
Continuation 保存继续执行的信息
    ↓
delay()
    ↓
CancellableContinuation
    ↓
Delay
    ↓
Handler.postDelayed()
    ↓
Android MessageQueue
    ↓
Looper
    ↓
Runnable
    ↓
Continuation.resumeUndispatched()
    ↓
状态机继续执行

到这里,我们实际上已经把:

"协程如何被外部事件唤醒"

这个问题真正看到了底层。

delay() 只是其中一个非常典型的例子。


十九、下一篇:Retrofit 又是怎么恢复协程的?

现在我们可以发现一个非常有意思的事情。

delay() 的恢复是:

scss 复制代码
协程
    ↓
挂起
    ↓
注册 Handler.postDelayed()
    ↓
时间到了
    ↓
Runnable
    ↓
resume()

但是 Retrofit 完全不一样。

例如:

kotlin 复制代码
interface Api {

    @GET("user")
    suspend fun getUser(): User
}

这里没有:

scss 复制代码
delay()

也没有:

scss 复制代码
postDelayed()

但是网络请求完成以后,协程同样会:

scss 复制代码
resume()

那么问题就来了:

Retrofit 是怎么拿到这个 Continuation 的?

以及:

网络请求完成以后,又是谁调用了 Continuation?

我们下一篇将从:

kotlin 复制代码
suspend fun getUser()

一路追到:

scss 复制代码
Retrofit
    ↓
HttpServiceMethod
    ↓
SuspendForBody
    ↓
KotlinExtensions.await()
    ↓
suspendCancellableCoroutine
    ↓
Continuation
    ↓
OkHttp Callback
    ↓
onResponse()
    ↓
continuation.resume()

到时候你会发现:

scss 复制代码
delay()

和:

复制代码
Retrofit

虽然一个是"等待时间",一个是"等待网络",但它们最终做的是同一件事情:

markdown 复制代码
挂起协程
    ↓
把 Continuation 交给一个异步系统
    ↓
异步事件发生
    ↓
调用 Continuation
    ↓
协程恢复

这也正是理解:

"协程的本质为什么可以看成回调"

最好的一个切入点。

而在 Retrofit 这一篇之后,我们再回头研究:

复制代码
CoroutineDispatcher

就会非常自然。

因为到那个时候,我们已经知道:

lua 复制代码
Continuation.resume()

究竟是从哪里来的。

接下来真正需要解决的问题就变成了:

Continuation 恢复以后,为什么有时候继续在当前线程执行,有时候却会被 Dispatcher 重新调度到另一个线程?

这才是 Dispatcher 真正需要解决的问题。

相关推荐
律宏阔1 小时前
Android 红外功能实现与红外协议开发笔记:ConsumerIrManager、NEC、PWM、曼彻斯特编码
android
mmsx2 小时前
MapLibre 统一图层模型:sealed class + 声明式图层 + XML 持久化
android·源码·地图·maplibre
YUJIANYUE2 小时前
查立得万用查分安卓版(web环境+查询系统免安装单文件一键运行包)
android·前端
天空之城--3 小时前
Android Launcher 性能优化完全指南:从启动到滑动,构建极致流畅的桌面体验
android·性能优化
hunterandroid3 小时前
Android ANR 排查实战:从线上告警到主线程卡点定位
android·前端
TimeFine4 小时前
智能眼镜开发:图片翻译与EXIF的重要性
android
律宏阔4 小时前
Android 车载蓝牙开发笔记:HFP、A2DP、AVRCP、PBAP、MAP、BLE 与系统 API
android·蓝牙
@Adzc4 小时前
Android 16 省电模式 Tile 点击导致状态栏收起
android
杉氧4 小时前
打破边界(二):在 React Native 中嵌入原生 UI 组件 (Native UI Components)
android·前端·react native