Kotlin 协程与挂起函数(Coroutines & suspend)入门到实战

Kotlin 协程是 Android 和后端 Kotlin 开发里最核心的异步方案之一。

很多人第一次学协程时会卡在几个地方:

  • suspend 到底是什么?
  • 协程是不是线程?
  • 为什么不会阻塞?
  • launchasyncwithContext 有什么区别?
  • Android 项目到底该怎么用?

这篇教程按**“从能看懂 → 能写 → 能实战”**的方式讲。


一、为什么需要协程?

先看传统代码:

fun downloadData() {
    Thread {
        Thread.sleep(3000)
        runOnUiThread {
            textView.text = "下载完成"
        }
    }.start()
}

问题:

  • 回调嵌套
  • 线程管理麻烦
  • 容易内存泄漏
  • 异步代码像"地狱"

协程的写法:

lifecycleScope.launch {
    delay(3000)
    textView.text = "下载完成"
}

是不是像同步代码?但它实际上是异步的

这就是协程最大的意义:

用同步代码的写法,完成异步操作。


二、什么是协程(Coroutine)

协程可以理解为:“轻量级线程”

但它不是线程

线程 vs 协程

对比 线程 协程
系统级
创建成本 极低
切换成本 很低
数量 可以很多
阻塞 容易 默认非阻塞
依赖关系 线程包含协程 协程运行在线程上

比如:

repeat(100000) {
    launch {
        delay(1000)
    }
}

10万个协程都没问题。但10万个线程直接炸。


三、挂起函数 suspend 到底是什么?

这是最关键的地方。

1. suspend 不是异步

很多人误解:

suspend fun test()

自动开线程
自动异步

suspend 的真正含义:

这个函数可以"暂停"而不阻塞线程。

2. 什么叫"挂起"?

suspend fun loadData() {
    delay(3000)
    println("完成")
}

这里 delay(3000) 会:

  • 暂停当前协程
  • 释放线程
  • 3秒后恢复

注意:线程没有被卡死。

3. Thread.sleep 和 delay 的区别

// Thread.sleep
Thread.sleep(3000)
// 特点:阻塞线程,什么都干不了

// delay
delay(3000)
// 特点:只暂停协程,不阻塞线程

直观理解

假设:

  • 线程 = 厨房
  • 协程 = 厨师
Thread.sleep delay
厨师睡觉 厨师说:“3秒后叫我”
厨房也废了 厨房还能给别人做饭

这就是协程高性能的核心。


四、协程的基本使用

先添加依赖:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"

五、launch:启动协程

GlobalScope.launch {
    delay(1000)
    println("协程执行")
}
launch 特点 说明
启动协程 无返回值
异步执行 类似 new Thread(),但更轻量

六、runBlocking(学习阶段使用)

fun main() = runBlocking {
    launch {
        delay(1000)
        println("协程")
    }
    println("开始")
}

输出:

开始
协程

runBlocking 是什么?

  • 作用: 阻塞当前线程,直到内部协程结束
  • 适合: 学习、测试
  • 不适合: Android 主线程

七、async 与 await

需要返回值时:

runBlocking {
    val result = async {
        delay(2000)
        "请求成功"
    }
    println(result.await())
}
async 特点 说明
有返回值 返回 Deferred
await() 等待结果

八、协程调度器 Dispatcher

协程运行在哪个线程?由 Dispatcher 决定。


九、常见 Dispatcher

1. Main

  • 主线程
  • Dispatchers.Main
  • 用于: 更新UI

2. IO

  • IO线程池
  • Dispatchers.IO
  • 用于: 网络、数据库、文件

3. Default

  • CPU密集型
  • Dispatchers.Default
  • 用于: 排序、JSON解析、大计算

十、withContext:线程切换

最常用。

lifecycleScope.launch {
    val result = withContext(Dispatchers.IO) {
        // 网络请求
        "服务器数据"
    }
    textView.text = result
}

执行流程:

主线程:launch
    ↓
切换IO线程:withContext(IO)
    ↓
执行完成
    ↓
自动回主线程

十一、协程作用域 CoroutineScope

协程必须运行在作用域里。


十二、GlobalScope 为什么不推荐?

GlobalScope.launch { }

问题:

  • 生命周期不可控
  • 容易内存泄漏
  • Activity销毁还在运行

所以: Android开发基本不用。


十三、Android 正确写法

lifecycleScope

// Activity
lifecycleScope.launch { }

// Fragment
viewLifecycleOwner.lifecycleScope.launch { }

特点: 页面销毁自动取消协程


十四、ViewModel 中使用

class MainViewModel : ViewModel() {
    fun load() {
        viewModelScope.launch {
            val data = withContext(Dispatchers.IO) {
                api.getData()
            }
        }
    }
}

这是 Android 官方推荐方案


十五、协程取消机制

val job = launch {
    repeat(100) {
        delay(1000)
        println(it)
    }
}

job.cancel()

为什么协程能取消? 因为 delay() 会检查取消状态。


十六、Job

每个协程都有 Job。

val job = launch { }

作用: canceljoin、管理生命周期


十七、join()

等待协程结束:

val job = launch {
    delay(2000)
}
job.join()
println("结束")

十八、异常处理

try-catch

launch {
    try {
        val data = api.load()
    } catch (e: Exception) {
        // 处理异常
    }
}

十九、SupervisorJob

普通情况下: 一个子协程崩了,全部取消。

SupervisorJob:

val scope = CoroutineScope(
    SupervisorJob() + Dispatchers.Main
)

特点: 一个失败,不影响其他协程。Android 很常用。


二十、协程常见面试题

1. suspend 和 coroutineScope 区别?

  • suspend — 只是说明函数可挂起
  • coroutineScope — 会创建协程作用域

2. launch 和 async 区别?

launch async
无返回值 有返回值
返回 Job 返回 Deferred

3. delay 为什么不卡线程?

因为:

  • 它会挂起协程
  • 不阻塞线程

4. 协程是不是线程?

不是。 协程运行在线程上。


二十一、Android 实战案例

场景1:网络请求

viewModelScope.launch {
    try {
        val data = withContext(Dispatchers.IO) {
            api.getUser()
        }
        tvName.text = data.name
    } catch (e: Exception) {
        toast("请求失败")
    }
}

场景2:并发请求

viewModelScope.launch {
    val userTask = async(Dispatchers.IO) {
        api.getUser()
    }
    val videoTask = async(Dispatchers.IO) {
        api.getVideo()
    }

    val user = userTask.await()
    val video = videoTask.await()
}

优势: 两个请求同时执行。

场景3:倒计时

lifecycleScope.launch {
    for (i in 10 downTo 0) {
        tv.text = "$i"
        delay(1000)
    }
}

二十二、Flow 与协程关系

很多人混淆。

协程 Flow
解决:一个异步任务 解决:连续的数据流

比如: 搜索输入、股票数据、聊天消息、Room数据库监听

简单例子:

flow {
    emit(1)
    delay(1000)
    emit(2)
}

二十三、协程学习路线(推荐)

建议顺序:

  1. launch
  2. suspend
  3. delay
  4. async / await
  5. Dispatcher
  6. withContext
  7. lifecycleScope
  8. viewModelScope
  9. Job
  10. Flow

二十四、协程核心理解(最重要)

suspend = 可以暂停协程,但不会阻塞线程

你就已经超过很多只会背API的人了。


二十五、实际开发最佳实践

1. 不要用 GlobalScope

改用:

  • viewModelScope
  • lifecycleScope

2. IO任务放 Dispatchers.IO

withContext(Dispatchers.IO)

3. UI更新必须 Main线程

Dispatchers.Main

4. Repository 不要持有 Scope

错误:

class Repository {
    val scope = CoroutineScope(...)
}

容易泄漏。


二十六、完整 Android MVVM 示例

ViewModel

class UserViewModel : ViewModel() {
    val userLiveData = MutableLiveData<User>()

    fun loadUser() {
        viewModelScope.launch {
            val user = withContext(Dispatchers.IO) {
                api.getUser()
            }
            userLiveData.value = user
        }
    }
}

Activity

viewModel.userLiveData.observe(this) {
    tvName.text = it.name
}

二十七、总结

核心 作用
suspend 挂起函数
launch 启动协程
async 异步返回值
delay 非阻塞等待
withContext 切线程
Dispatcher 指定线程
viewModelScope Android推荐作用域
Flow 数据流

最后一段(真正理解协程)

很多人学协程,只会背:

  • launch
  • async
  • withContext

但真正重要的是:

协程的本质不是"开线程"。

而是:用极低成本管理大量异步任务。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐