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 仓库阅读完整源码实现

相关推荐
TechNomad13 小时前
Kotlin_Lambda编程详解
android·kotlin
熊猫_豆豆13 小时前
QT6 Android C++ 自制美观闹钟
android·c++·qt·闹钟
美狐美颜SDK开放平台13 小时前
Android/iOS直播APP平台开发中的视频美颜SDK优化技巧
android·ios·音视频·美颜sdk·第三方美颜sdk
聚美智数16 小时前
黄历查询-运势查询-国际法定节假日查询-API接口介绍
android·java·数据库
私人珍藏库18 小时前
[Android] 一木百宝箱-万能百宝箱+抖音去水印等几百种功能
android·人工智能·app·软件·多功能
wupa18 小时前
在 enableEdgeToEdge 模式下处理软键盘与自定义面板协同
android
plainGeekDev20 小时前
ProGuard → R8
android·java·kotlin
雨白20 小时前
C 语言字符串:从字符数组、指针到核心操作实战
android
Java小白笔记21 小时前
MySQL中存储过程大表分批删除历史数据
android·mysql·adb