Android 面试系列:Kotlin 协程的 delay 到底发生在哪个线程?

在开发中,我们经常写出这样的代码:

kotlin 复制代码
mainScope.launch {
    log("start")
    delay(1000)
    log("end")
}

这段代码看起来非常简单,但它隐藏了一个非常经典的问题:

delay 这 1 秒到底发生在哪个线程? 主线程前后都在执行,那中间谁在"等时间"?


结论

delay 从来不占用线程等待,它是一次"挂起 + 时间注册 + 调度恢复 + 状态机推进"的过程。

中间没有任何业务线程在 sleep,也没有线程在阻塞计时。

可以先运行我的 Demo:

👉 Delay 实现原理(自制 EventLoop) (非 Android 环境)


🧩 一、delay 的四阶段模型

为了彻底理解 delay,我们将其拆解为底层运作的四个连续阶段:


🧩 第一阶段:Suspend(挂起)

当代码执行到 delay(1000) 时,协程会立即触发挂起:

1. 保存 Continuation

后续的业务代码(如 log("end"))会被作为挂起点的剩余逻辑,封装成一个 Continuation(即状态机对象)。

2. 线程立即释放

当前线程(如 UI 主线程)发生的是:

  • ❌ 没有 Thread.sleep()
  • ❌ 没有死循环阻塞
  • ❌ 没有 CPU 空转

当前协程直接 "保存状态并释放当前线程的执行权"。如果是 Android 主线程,它会立刻回到底层的消息循环去处理其他 UI 绘制或点击事件,绝对不会卡顿。


🧩 第二阶段:Schedule(时间注册)

既然线程释放了,总得有人负责"记住这 1 秒"。这一阶段,协程会将刚刚封装好的恢复任务注册到对应环境的时间队列中

🌟 情况 1:Android 环境(Dispatchers.Main)

协程最终会把任务交给 Android 的底层消息机制:

kotlin 复制代码
handler.sendMessageDelayed(msg, 1000)
  • 谁在等? 并不是协程在等,而是 Android 系统的 MessageQueue 配合底层的 nativePollOnce() (epoll 机制) 在管理时间。

🌟 情况 2:纯 JVM / 非 Android 环境

如果没有 Android Looper,delay 会 Fallback 降级到协程全局的:

  • DefaultExecutor :一个全局的延迟守护线程。它通过 LockSupport.parkNanos() 配合内部的延迟任务队列(时间轮或最小堆结构)来统一管理延迟。

本质 :无论是哪种环境,都是将任务托管给了底层的调度系统/守护线程推进时间。


🧩 第三阶段:Dispatch(调度恢复)

当 1000ms 时间到了之后,底层的计时器(LooperDefaultExecutor)会触发回调。但注意:此时并不是直接盲目地去跑 log("end")

任务会先经过:

kotlin 复制代码
Dispatcher.dispatch()
  • 为什么要这一步? 因为计时器触发时所在的线程,未必是协程原本要求的线程(例如 DefaultExecutor 是一个独立的守护线程)。
  • 核心职责Dispatcher 负责重新检查协程的上下文,并决定"在哪个线程去恢复这个协程"。它会将恢复任务重新 post(投递)回目标线程的任务队列中(例如重新投递回 Android 的主线程队列)。

🧩 第四阶段:Resume(恢复状态机)

目标线程消费到这个调度任务后,协程框架重新接管控制权。

1. 恢复 Continuation

调用 continuation.resume(Unit),重新唤醒先前保存的协程状态机。

2. 执行剩余代码

协程从挂起点向下推进,最终执行:

kotlin 复制代码
log("end")

整个生命周期至此完美闭环。


二、用一个自制 EventLoop 看清 delay 本质

为了彻底验证并看清上述四个阶段,我写了这套 Demo 来完美还原一个"协程 delay 调度系统"。

1. Looper(事件循环)

kotlin 复制代码
Looper.prepare()
Looper.loop()
  • 本质 :一个 while(true) 的消息循环,用于模拟线程不阻塞时的任务消费。

2. Handler(任务投递)

kotlin 复制代码
fun sendMessageDelayed(msg: Message, delayMillis: Long)
  • 本质:负责把任务放入队列,并指定未来的执行时间。

3. Message(任务载体)

kotlin 复制代码
class Message {
    var whenTime: Long = 0L
    var callback: Runnable? = null
    var target: Handler? = null
}
  • 本质:一个"未来要执行的动作包装"。

4. CoroutineDispatcher(协程桥接)

kotlin 复制代码
class HandlerDispatcher(
    private val handler: Handler
) : CoroutineDispatcher(), Delay

三、delay 的真实实现(核心拦截)

在我的自制调度器中,delay 的核心对接点在于实现 scheduleResumeAfterDelay 接口:

kotlin 复制代码
override fun scheduleResumeAfterDelay(
    timeMillis: Long,
    continuation: CancellableContinuation<Unit>
) {
    // 【对应 Dispatch & Resume】把"恢复协程"封装成 Runnable 任务
    val block = Runnable {
        with(continuation) {
            // 官方的标准实现会经过 dispatcher.dispatch 重新分发线程
            // 这里在 Looper 线程直接恢复
            resumeUndispatched(Unit) 
        }
    }

    // 【对应 Schedule】封装成 Message,丢进延迟队列
    val msg = Message().apply {
        callback = block
    }
    handler.sendMessageDelayed(msg, timeMillis)
}

🔥 四、完整 delay 执行链路图

text 复制代码
launch (启动协程)
   ↓
delay(1000)
   ↓
【1. Suspend】挂起协程(保存 Continuation 状态,释放当前线程)
   ↓
【2. Schedule】调用 scheduleResumeAfterDelay → 封装 Message(callback = resume) 投入队列
   ↓
【底层的等待】MessageQueue / DefaultExecutor 在不阻塞业务线程的前提下推进时间
   ↓
【3. Dispatch】时间到!Looper 捞出消息,通过 Dispatcher 分发,决定在哪个线程恢复
   ↓
【4. Resume】调用 continuation.resume() 恢复状态机
   ↓
继续执行 "end"

五、结论

  • **delay 的本质是:状态机挂起(Suspend) →\rightarrow → 时间注册(Schedule) →\rightarrow → 线程分发(Dispatch) →\rightarrow → 回调恢复(Resume)
  • 整个挂起期间,没有线程 sleep、没有线程阻塞、没有 CPU 空转,有的只是调度系统在极其高效地推进时间。

🚀 六、这套demo的价值

这套代码虽然是一个 Demo,但它它用最精简的代码表达了:EventLoopMessageQueueDispatcherDelay 以及 Continuation 之间的闭环协作。

👉 欢迎去我的 GitHub 仓库阅读完整源码实现

相关推荐
xingxiliang4 小时前
深入浅出 Android CTS 视频测试:从环境搭建、用例设计到底层通信原理
android·音视频
我命由我123454 小时前
Android 开发问题:为 PDFView 设置一个带有黑色边框的背景 drawable,但边框没有生效
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
雨白6 小时前
深入理解 Kotlin 协程 (八):拾遗补阙,探秘官方框架的调度细节与取消闭环
android·kotlin
海天鹰7 小时前
PHP上传文件
android·开发语言·php
2501_915918418 小时前
详解iOS App上架至App Store的全流程步骤与注意事项
android·macos·ios·小程序·uni-app·cocoa·iphone
码农coding10 小时前
android12 SystemUI之StatusBar(二)
android
Lesile11 小时前
Interview#1 历史演进:MVC · MVP · MVVM · MVI架构详解
android·android jetpack
杉氧12 小时前
Flutter 像素级还原实战:用 CustomPaint 与 Bezier 曲线手绘精致图针
android·前端·flutter
我命由我1234515 小时前
Android 在构建过程中,发现有两个依赖库都包含了相同路径的资源文件
android·java·开发语言·java-ee·kotlin·android-studio·android runtime
Coffeeee15 小时前
谷歌的一个优化建议,让我重新学了一遍Android里面如何正确处理位图
android·google·kotlin