Kotlin 协程与挂起函数(Coroutines & suspend)入门到实战
Kotlin 协程是 Android 和后端 Kotlin 开发里最核心的异步方案之一。
很多人第一次学协程时会卡在几个地方:
suspend到底是什么?- 协程是不是线程?
- 为什么不会阻塞?
launch、async、withContext有什么区别?- Android 项目到底该怎么用?
这篇教程按**"从能看懂 → 能写 → 能实战"**的方式讲。
一、为什么需要协程?
先看传统代码:
kotlin
fun downloadData() {
Thread {
Thread.sleep(3000)
runOnUiThread {
textView.text = "下载完成"
}
}.start()
}
问题:
- 回调嵌套
- 线程管理麻烦
- 容易内存泄漏
- 异步代码像"地狱"
协程的写法:
kotlin
lifecycleScope.launch {
delay(3000)
textView.text = "下载完成"
}
是不是像同步代码?但它实际上是异步的。
这就是协程最大的意义:
用同步代码的写法,完成异步操作。
二、什么是协程(Coroutine)
协程可以理解为:"轻量级线程"
但它不是线程。
线程 vs 协程
| 对比 | 线程 | 协程 |
|---|---|---|
| 系统级 | 是 | 否 |
| 创建成本 | 高 | 极低 |
| 切换成本 | 高 | 很低 |
| 数量 | 少 | 可以很多 |
| 阻塞 | 容易 | 默认非阻塞 |
| 依赖关系 | 线程包含协程 | 协程运行在线程上 |
比如:
kotlin
repeat(100000) {
launch {
delay(1000)
}
}
10万个协程都没问题。但10万个线程直接炸。
三、挂起函数 suspend 到底是什么?
这是最关键的地方。
1. suspend 不是异步
很多人误解:
kotlin
suspend fun test()
≠ 自动开线程
≠ 自动异步
suspend 的真正含义:
这个函数可以"暂停"而不阻塞线程。
2. 什么叫"挂起"?
kotlin
suspend fun loadData() {
delay(3000)
println("完成")
}
这里 delay(3000) 会:
- 暂停当前协程
- 释放线程
- 3秒后恢复
注意:线程没有被卡死。
3. Thread.sleep 和 delay 的区别
kotlin
// Thread.sleep
Thread.sleep(3000)
// 特点:阻塞线程,什么都干不了
// delay
delay(3000)
// 特点:只暂停协程,不阻塞线程
直观理解
假设:
- 线程 = 厨房
- 协程 = 厨师
Thread.sleep |
delay |
|---|---|
| 厨师睡觉 | 厨师说:"3秒后叫我" |
| 厨房也废了 | 厨房还能给别人做饭 |
这就是协程高性能的核心。
四、协程的基本使用
先添加依赖:
kotlin
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
五、launch:启动协程
kotlin
GlobalScope.launch {
delay(1000)
println("协程执行")
}
| launch 特点 | 说明 |
|---|---|
| 启动协程 | 无返回值 |
| 异步执行 | 类似 new Thread(),但更轻量 |
六、runBlocking(学习阶段使用)
kotlin
fun main() = runBlocking {
launch {
delay(1000)
println("协程")
}
println("开始")
}
输出:
开始
协程
runBlocking 是什么?
- 作用: 阻塞当前线程,直到内部协程结束
- 适合: 学习、测试
- 不适合: Android 主线程
七、async 与 await
需要返回值时:
kotlin
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:线程切换
最常用。
kotlin
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
// 网络请求
"服务器数据"
}
textView.text = result
}
执行流程:
主线程:launch
↓
切换IO线程:withContext(IO)
↓
执行完成
↓
自动回主线程
十一、协程作用域 CoroutineScope
协程必须运行在作用域里。
十二、GlobalScope 为什么不推荐?
kotlin
GlobalScope.launch { }
问题:
- 生命周期不可控
- 容易内存泄漏
- Activity销毁还在运行
所以: Android开发基本不用。
十三、Android 正确写法
lifecycleScope
kotlin
// Activity
lifecycleScope.launch { }
// Fragment
viewLifecycleOwner.lifecycleScope.launch { }
特点: 页面销毁自动取消协程
十四、ViewModel 中使用
kotlin
class MainViewModel : ViewModel() {
fun load() {
viewModelScope.launch {
val data = withContext(Dispatchers.IO) {
api.getData()
}
}
}
}
这是 Android 官方推荐方案。
十五、协程取消机制
kotlin
val job = launch {
repeat(100) {
delay(1000)
println(it)
}
}
job.cancel()
为什么协程能取消? 因为 delay() 会检查取消状态。
十六、Job
每个协程都有 Job。
kotlin
val job = launch { }
作用: cancel、join、管理生命周期
十七、join()
等待协程结束:
kotlin
val job = launch {
delay(2000)
}
job.join()
println("结束")
十八、异常处理
try-catch
kotlin
launch {
try {
val data = api.load()
} catch (e: Exception) {
// 处理异常
}
}
十九、SupervisorJob
普通情况下: 一个子协程崩了,全部取消。
SupervisorJob:
kotlin
val scope = CoroutineScope(
SupervisorJob() + Dispatchers.Main
)
特点: 一个失败,不影响其他协程。Android 很常用。
二十、协程常见面试题
1. suspend 和 coroutineScope 区别?
- suspend --- 只是说明函数可挂起
- coroutineScope --- 会创建协程作用域
2. launch 和 async 区别?
| launch | async |
|---|---|
| 无返回值 | 有返回值 |
| 返回 Job | 返回 Deferred |
3. delay 为什么不卡线程?
因为:
- 它会挂起协程
- 不阻塞线程
4. 协程是不是线程?
不是。 协程运行在线程上。
二十一、Android 实战案例
场景1:网络请求
kotlin
viewModelScope.launch {
try {
val data = withContext(Dispatchers.IO) {
api.getUser()
}
tvName.text = data.name
} catch (e: Exception) {
toast("请求失败")
}
}
场景2:并发请求
kotlin
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:倒计时
kotlin
lifecycleScope.launch {
for (i in 10 downTo 0) {
tv.text = "$i"
delay(1000)
}
}
二十二、Flow 与协程关系
很多人混淆。
| 协程 | Flow |
|---|---|
| 解决:一个异步任务 | 解决:连续的数据流 |
比如: 搜索输入、股票数据、聊天消息、Room数据库监听
简单例子:
kotlin
flow {
emit(1)
delay(1000)
emit(2)
}
二十三、协程学习路线(推荐)
建议顺序:
launchsuspenddelayasync/awaitDispatcherwithContextlifecycleScopeviewModelScopeJobFlow
二十四、协程核心理解(最重要)
suspend = 可以暂停协程,但不会阻塞线程
你就已经超过很多只会背API的人了。
二十五、实际开发最佳实践
1. 不要用 GlobalScope
改用:
viewModelScopelifecycleScope
2. IO任务放 Dispatchers.IO
kotlin
withContext(Dispatchers.IO)
3. UI更新必须 Main线程
kotlin
Dispatchers.Main
4. Repository 不要持有 Scope
错误:
kotlin
class Repository {
val scope = CoroutineScope(...)
}
容易泄漏。
二十六、完整 Android MVVM 示例
ViewModel
kotlin
class UserViewModel : ViewModel() {
val userLiveData = MutableLiveData<User>()
fun loadUser() {
viewModelScope.launch {
val user = withContext(Dispatchers.IO) {
api.getUser()
}
userLiveData.value = user
}
}
}
Activity
kotlin
viewModel.userLiveData.observe(this) {
tvName.text = it.name
}
二十七、总结
| 核心 | 作用 |
|---|---|
suspend |
挂起函数 |
launch |
启动协程 |
async |
异步返回值 |
delay |
非阻塞等待 |
withContext |
切线程 |
Dispatcher |
指定线程 |
viewModelScope |
Android推荐作用域 |
Flow |
数据流 |
最后一段(真正理解协程)
很多人学协程,只会背:
launchasyncwithContext
但真正重要的是:
协程的本质不是"开线程"。
而是:用极低成本管理大量异步任务。