1. 协程基础回顾:协程是什么、为什么需要协程、协程与线程的区别、协程的基本构建块
好,咱们开始第一讲。说实话,每次带新人或者做技术分享,我都是从这一节讲起。协程这东西,刚接触时觉得玄乎,用熟了又觉得理所当然。但如果你让我用一句话说清楚——协程是轻量级的异步任务管理框架,它让异步代码写起来像同步一样自然。
1.1 协程是什么?
协程,英文叫 Coroutine。你可以把它理解成可挂起、可恢复的计算单元。什么意思呢?就是一段代码跑着跑着,可以暂停下来,等某个耗时操作(比如网络请求、数据库读写)完成了,再接着往下跑。
我刚开始学的时候,总觉得这跟线程差不多啊。其实差别大了去了。协程是协作式的,它主动让出 CPU;线程是抢占式的,由操作系统调度。说白了,协程是自己控制什么时候停、什么时候走,线程是被系统强行切换的。
核心要点:协程不是线程的替代品,而是线程的管理工具。它帮你在合适的线程上执行合适的任务,同时避免回调地狱。
1.2 为什么需要协程?
你想想看,Android 开发中我们经常遇到这样的场景:
- 从网络拉取用户信息
- 从本地数据库读取缓存
- 更新 UI
这三步有先后依赖关系。如果用传统的回调方式,代码会变成这样:
api.fetchUserInfo(userId) { user ->
db.saveUser(user) {
runOnUiThread {
textView.text = user.name
}
}
}
这就是经典的「回调地狱」。代码嵌套越来越深,逻辑越来越乱。我曾在项目里见过七层回调嵌套的代码,调试时简直想砸电脑。
协程的出现,就是为了解决这个问题。它让你用顺序写代码的方式,表达异步执行的逻辑:
suspend fun loadUser(userId: String) {
val user = api.fetchUserInfo(userId) // 挂起,不阻塞线程
db.saveUser(user) // 挂起,不阻塞线程
textView.text = user.name // 自动切回主线程
}
看到了吗?代码从上往下读,逻辑清清楚楚。这就是协程的魅力。
1.3 协程与线程的区别
这个问题面试常问,我也被问过很多次。我习惯用一个比喻来解释:
- 线程就像一辆公交车,有固定的路线和站点,乘客(任务)上下车由司机(操作系统)决定。
- 协程就像一辆出租车,乘客(任务)自己决定什么时候停、什么时候走,灵活得多。
具体区别,我整理了一张表:
| 对比维度 | 线程 | 协程 |
|---|---|---|
| 调度方式 | 操作系统抢占式调度 | 用户态协作式调度 |
| 创建成本 | 高(约 1MB 栈空间) | 极低(约 1KB 内存) |
| 切换开销 | 涉及内核态切换,开销大 | 纯用户态切换,开销极小 |
| 并发数量 | 几百个就吃力了 | 轻松上万 |
| 代码可读性 | 回调嵌套,难以维护 | 顺序编写,清晰易懂 |
我的经验:在 Android 中,主线程负责 UI 更新,不能阻塞。协程让你在后台线程执行耗时操作,然后自动切回主线程更新 UI。这个「自动切线程」的能力,是协程最实用的特性之一。
1.4 协程的基本构建块
协程有三个最常用的构建块:launch、async 和 runBlocking。我一个个说。
1.4.1 launch:只管发射,不管结果
launch 用于启动一个不关心返回值的协程。比如记录日志、发送统计、更新缓存等。
scope.launch {
// 这是一个协程
val data = fetchData() // 挂起函数
saveToCache(data) // 挂起函数
// 不需要返回结果
}
我在项目中常用 launch 来处理「发后即忘」的任务。比如用户点击按钮后,我们记录一次点击事件,不需要等待结果。
1.4.2 async:要结果,就用它
当你需要协程返回一个结果时,用 async。它返回一个 Deferred 对象,你可以通过 await() 获取结果。
scope.launch {
val deferred = async {
fetchUserInfo(userId) // 返回 User 对象
}
// 做其他事情...
val user = deferred.await() // 等待结果
updateUI(user)
}
我习惯用 async 做并行任务。比如同时请求用户信息和商品列表,等两个都完成后一起处理:
scope.launch {
val userDeferred = async { api.fetchUser() }
val productDeferred = async { api.fetchProducts() }
val user = userDeferred.await()
val products = productDeferred.await()
showData(user, products)
}
注意:千万不要在 async 块里直接调用 await(),那样就变成串行了。正确的做法是:先启动所有 async,再统一 await()。
1.4.3 runBlocking:桥接世界
runBlocking 是一个特殊的构建块。它会阻塞当前线程,直到协程执行完毕。这通常只用在 main 函数或测试代码中。
fun main() {
runBlocking {
delay(1000)
println("Hello from coroutine!")
}
// 这里会等协程执行完才执行
println("Done")
}
我曾经在项目里看到有人把 runBlocking 用在 Android 的 Activity 里,结果导致 ANR。记住:不要在 Android 主线程使用 runBlocking,它会阻塞 UI 渲染。
1.5 知识体系总览
为了让你对本章内容有个整体印象,我画了一张图:
1.6 避坑指南
最后,分享几个我踩过的坑:
- 不要滥用 GlobalScope:它不会自动取消,容易造成内存泄漏。我习惯用
viewModelScope或自定义CoroutineScope。 - 注意协程的取消:协程取消是协作式的,需要检查
isActive或调用ensureActive()。 - 异常处理要到位:
launch中的异常会直接抛出,async中的异常在await()时抛出。建议用try-catch或CoroutineExceptionHandler统一处理。
我的习惯:在团队里,我要求所有协程代码必须明确指定 CoroutineScope 和 Dispatcher。这样代码可读性高,也方便 review。
好了,这一章就到这里。协程的基础概念和三个构建块,你掌握了吗?下一章我们深入聊聊挂起函数和协程作用域,这些东西在实际项目中天天用。
公众号:蓝海资料掘金营,微信deep3321