Kotlin 协程与挂起函数(Coroutines & suspend)入门到实战
Kotlin 协程是 Android 和后端 Kotlin 开发里最核心的异步方案之一。
很多人第一次学协程时会卡在几个地方:
suspend到底是什么?- 协程是不是线程?
- 为什么不会阻塞?
launch、async、withContext有什么区别?- Android 项目到底该怎么用?
这篇教程按**“从能看懂 → 能写 → 能实战”**的方式讲。
一、为什么需要协程?
先看传统代码:
fundownloadData(){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 不是异步
很多人误解:
suspendfuntest()≠自动开线程
≠自动异步
suspend的真正含义:
这个函数可以"暂停"而不阻塞线程。
2. 什么叫"挂起"?
suspendfunloadData(){delay(3000)println("完成")}这里delay(3000)会:
- 暂停当前协程
- 释放线程
- 3秒后恢复
注意:线程没有被卡死。
3. Thread.sleep 和 delay 的区别
// Thread.sleepThread.sleep(3000)// 特点:阻塞线程,什么都干不了// delaydelay(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(学习阶段使用)
funmain()=runBlocking{launch{delay(1000)println("协程")}println("开始")}输出:
开始 协程runBlocking是什么?
- 作用:阻塞当前线程,直到内部协程结束
- 适合:学习、测试
- 不适合:Android 主线程
七、async 与 await
需要返回值时:
runBlocking{valresult=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{valresult=withContext(Dispatchers.IO){// 网络请求"服务器数据"}textView.text=result}执行流程:
主线程:launch ↓ 切换IO线程:withContext(IO) ↓ 执行完成 ↓ 自动回主线程十一、协程作用域 CoroutineScope
协程必须运行在作用域里。
十二、GlobalScope 为什么不推荐?
GlobalScope.launch{}问题:
- 生命周期不可控
- 容易内存泄漏
- Activity销毁还在运行
所以:Android开发基本不用。
十三、Android 正确写法
lifecycleScope
// ActivitylifecycleScope.launch{}// FragmentviewLifecycleOwner.lifecycleScope.launch{}特点:页面销毁自动取消协程
十四、ViewModel 中使用
classMainViewModel:ViewModel(){funload(){viewModelScope.launch{valdata=withContext(Dispatchers.IO){api.getData()}}}}这是Android 官方推荐方案。
十五、协程取消机制
valjob=launch{repeat(100){delay(1000)println(it)}}job.cancel()为什么协程能取消?因为delay()会检查取消状态。
十六、Job
每个协程都有 Job。
valjob=launch{}作用:cancel、join、管理生命周期
十七、join()
等待协程结束:
valjob=launch{delay(2000)}job.join()println("结束")十八、异常处理
try-catch
launch{try{valdata=api.load()}catch(e:Exception){// 处理异常}}十九、SupervisorJob
普通情况下:一个子协程崩了,全部取消。
SupervisorJob:
valscope=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{valdata=withContext(Dispatchers.IO){api.getUser()}tvName.text=data.name}catch(e:Exception){toast("请求失败")}}场景2:并发请求
viewModelScope.launch{valuserTask=async(Dispatchers.IO){api.getUser()}valvideoTask=async(Dispatchers.IO){api.getVideo()}valuser=userTask.await()valvideo=videoTask.await()}优势:两个请求同时执行。
场景3:倒计时
lifecycleScope.launch{for(iin10downTo0){tv.text="$i"delay(1000)}}二十二、Flow 与协程关系
很多人混淆。
| 协程 | Flow |
|---|---|
| 解决:一个异步任务 | 解决:连续的数据流 |
比如:搜索输入、股票数据、聊天消息、Room数据库监听
简单例子:
flow{emit(1)delay(1000)emit(2)}二十三、协程学习路线(推荐)
建议顺序:
launchsuspenddelayasync/awaitDispatcherwithContextlifecycleScopeviewModelScopeJobFlow
二十四、协程核心理解(最重要)
suspend = 可以暂停协程,但不会阻塞线程
你就已经超过很多只会背API的人了。
二十五、实际开发最佳实践
1. 不要用 GlobalScope
改用:
viewModelScopelifecycleScope
2. IO任务放 Dispatchers.IO
withContext(Dispatchers.IO)3. UI更新必须 Main线程
Dispatchers.Main4. Repository 不要持有 Scope
错误:
classRepository{valscope=CoroutineScope(...)}容易泄漏。
二十六、完整 Android MVVM 示例
ViewModel
classUserViewModel:ViewModel(){valuserLiveData=MutableLiveData<User>()funloadUser(){viewModelScope.launch{valuser=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 | 数据流 |
最后一段(真正理解协程)
很多人学协程,只会背:
launchasyncwithContext
但真正重要的是:
协程的本质不是"开线程"。
而是:用极低成本管理大量异步任务。