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()
那么现在问题变得非常具体:
HandlerContext的scheduleResumeAfterDelay()是怎么实现的?
答案非常简单。
七、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 真正需要解决的问题。