深入理解 Kotlin 协程:从入门到实战
前言
如果你问 Android 或后端 Kotlin 开发者,过去几年里什么技术改变了他们的编程方式,协程(Coroutines)一定名列前茅。协程让异步编程变得像写同步代码一样自然,彻底告别了回调地狱。
但协程并不简单。很多开发者用了很久,仍然停留在"会用但不懂"的阶段——遇到问题时不知道为什么,出了 bug 时不知道怎么排查。
这篇博客的目标是:让你真正理解协程,而不只是会用它。
第一章:协程是什么?
1.1 协程的定义
协程(Coroutine)这个词来自计算机科学,最早可以追溯到 1958 年。它的全称是 Co-routine,即"协作式例程"。
官方对 Kotlin 协程的定义是:
协程是一种可以被挂起和恢复的计算实例(instance of suspendable computation)。
用更直白的话说:协程,本质上是"可以在执行过程中暂停,并在未来某个时刻继续执行"的代码块。
Kotlin 协程是一种轻量级线程框架,本质上是**挂起函数(suspend functions)**的调度机制。它不是线程,但可以运行在线程上,核心目标是:减少线程切换成本、简化异步代码、提升可读性。
它让你用顺序代码的方式写异步逻辑,解决回调地狱问题。
1.2 为什么叫"轻量级线程"?
这里需要区分几个概念,很多初学者会混淆:
- 线程:操作系统级别的概念,创建和切换都有较大开销,一个进程通常只能创建几千个线程。
- 协程:用户态的调度单元,创建成本极低,一个应用中可以轻松运行几十万个协程。
协程运行在线程之上。一个线程可以运行多个协程,当一个协程挂起时,线程去执行其他协程,而不是傻等着。这就是"轻量"的本质。
// 创建 10 万个协程,完全没问题
fun main() = runBlocking {
val jobs = (1..100_000).map {
launch {
delay(1000)
}
}
jobs.forEach { it.join() }
println("10 万个协程全部完成")
}
如果换成线程,这段代码会直接 OOM(内存溢出)崩溃。
| 对比项 | 线程 | 协程 |
|---|---|---|
| 创建成本 | 高(内核级资源,约 1MB 栈) | 极低(用户态,初始仅几十字节) |
| 数量上限 | 通常几千个 | 可以几十万个 |
| 切换开销 | 大(需要操作系统介入) | 极小(用户态切换) |
| 阻塞行为 | 阻塞整个线程 | 挂起自身,线程继续工作 |
| 代码风格 | 回调/Future | 顺序、直观 |
| 取消机制 | 复杂,需要手动管理 | 结构化取消,自动管理 |
1.3 协程的核心思想:挂起(suspend)而不是阻塞(block)
这是理解协程最关键的一句话。
阻塞:线程停在这里,什么也不做,等你完成。
挂起:协程暂停,让出线程,线程去做其他事,等你完成后协程再恢复。
阻塞模型:
线程 ——执行——[等待网络中...........停在这里不动]——继续执行——>
挂起模型:
协程A ——执行——[挂起] [恢复]——继续执行——>
线程 ——执行—————[执行协程B]——[执行协程C]——[恢复A]——>
第二章:为什么需要协程?
2.1 从一个真实问题说起
假设你在写一个 Android 应用,需要:
- 从网络请求用户信息
- 根据用户信息查询数据库
- 将结果显示到 UI
用传统的线程+回调方式,代码会长这样:
// 回调地狱的典型形态
fun loadUserData() {
fetchUserFromNetwork { user ->
if (user != null) {
queryDatabase(user.id) { dbResult ->
if (dbResult != null) {
runOnUiThread {
updateUI(dbResult)
}
} else {
runOnUiThread {
showError("数据库查询失败")
}
}
}
} else {
runOnUiThread {
showError("网络请求失败")
}
}
}
}
这还只是两步操作。如果有五步、十步,代码会嵌套得让人崩溃。更糟糕的是:
- 错误处理散落在各处,极难维护
- 取消操作几乎无法实现
- 生命周期管理全靠手动,容易泄漏
2.2 用协程改写
// 用协程改写后,代码像同步的,但实际上是异步的
fun loadUserData() {
lifecycleScope.launch {
try {
val user = fetchUserFromNetwork() // 挂起,不阻塞线程
val dbResult = queryDatabase(user.id) // 挂起,不阻塞线程
updateUI(dbResult) // 自动在主线程执行
} catch (e: Exception) {
showError(e.message)
}
}
}
代码变成了线性的、清晰的,错误处理也集中了,生命周期也由 lifecycleScope 自动管理。这就是协程的魅力。
第三章:协程的核心概念
3.1 挂起函数(suspend function)
挂起函数是协程的基础单元。用 suspend 关键字修饰的函数,只能在协程或其他挂起函数中调用。
// 这是一个挂起函数
suspend fun fetchUserFromNetwork(): User {
delay(1000) // delay 本身也是挂起函数,挂起 1 秒但不阻塞线程
return User(id = 1, name = "张三")
}
关键理解:suspend 并不意味着函数一定会挂起,它只是表示"这个函数有能力挂起"。编译器会在底层把挂起函数转换为一个状态机(State Machine),使得函数能在挂起点保存当前状态、暂停执行,并在恢复时从断点继续。
你可以把挂起函数想象成一个可以"暂停"的任务:
开始执行 → 遇到网络请求 → 【挂起,让出线程】→ 网络回来了 → 【恢复】→ 继续执行 → 完成
挂起函数只能在协程或另一个挂起函数中调用,如果你在普通函数里调用挂起函数,编译器会报错。这个限制是刻意的——它保证了挂起能力的"传染性",形成清晰的边界。
3.2 协程构建器
协程需要用构建器来启动。最常用的有三个:launch、async、runBlocking。
launch:启动一个不需要返回值的协程
val job = scope.launch {
println("协程开始")
delay(1000)
println("协程结束")
}
// Job 代表协程本身,可以用来等待或取消
job.join() // 等待协程完成
job.cancel() // 取消协程
async:启动一个需要返回值的协程
// async 返回 Deferred<T>,可以获取结果
val deferred = scope.async {
delay(1000)
42 // 返回值
}
val result = deferred.await() // 等待并获取结果
println("结果是:$result") // 结果是:42
runBlocking:慎用,主要用于测试和顶层入口
fun main() = runBlocking {
val result = fetchUserFromNetwork()
println(result.name)
}
runBlocking 在实际开发中几乎不使用。 它会阻塞当前线程,和协程"挂起而不阻塞"的设计哲学背道而驰。它的用途主要有两个:
- 在
main函数中作为最顶层的协程入口(命令行程序或服务端启动) - 在单元测试中使用(后来被
runTest取代)
在 Android 开发中,你几乎不应该写 runBlocking。看到项目里有大量 runBlocking 的代码,通常意味着开发者对协程理解不够深入,把它当成了"让挂起函数能在普通函数里调用"的工具,这种用法是反模式的。
3.3 协程调度器(Dispatchers)
调度器决定协程在哪个线程或线程池上运行:
// Dispatchers.Main:主线程(只能做 UI 操作)
launch(Dispatchers.Main) {
textView.text = "更新 UI"
}
// Dispatchers.IO:IO 密集型操作(网络、文件、数据库)
// 背后是一个大线程池,默认最多 64 个线程
launch(Dispatchers.IO) {
val data = readFromFile()
}
// Dispatchers.Default:CPU 密集型操作(计算、排序、JSON 解析)
// 背后是 CPU 核心数量的线程池
launch(Dispatchers.Default) {
val result = sortBigList()
}
// Dispatchers.Unconfined:不限制线程(几乎不用,除非你非常清楚自己在做什么)
一个典型的使用模式是用 withContext 切换线程:
// 整个函数在主线程启动,内部自动切换
lifecycleScope.launch {
showLoading()
val result = withContext(Dispatchers.IO) {
// 自动切换到 IO 线程
fetchDataFromNetwork()
}
// 自动切回主线程
hideLoading()
displayResult(result)
}
withContext 是协程内部切换线程的标准方式。它会挂起当前协程,在指定调度器上执行代码块,完成后切回原来的调度器,整个过程对调用方完全透明。
第四章:协程作用域——Android 开发的关键
这是 Android 开发中最容易踩坑、也最需要深入理解的部分。
4.1 为什么需要作用域?
协程不能凭空存在,它必须依附于某个作用域(CoroutineScope)。作用域的职责是:管理其内部所有协程的生命周期。
当作用域被取消时,它内部所有正在运行的协程都会被取消。这个机制防止了资源泄漏——你不需要手动追踪每一个协程。
val scope = CoroutineScope(Dispatchers.IO)
scope.launch { /* 协程 A */ }
scope.launch { /* 协程 B */ }
scope.launch { /* 协程 C */ }
scope.cancel() // A、B、C 全部取消,一行搞定
4.2 Android 中的三大作用域
在 Android 实际开发中,你几乎不需要手动创建 CoroutineScope。Jetpack 已经提供了和 Android 组件生命周期绑定的现成作用域:
viewModelScope——ViewModel 层的首选
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState = _uiState.asStateFlow()
init {
loadUser()
}
fun loadUser() {
viewModelScope.launch {
try {
val user = repository.getUser()
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "未知错误")
}
}
}
// ViewModel 被销毁(onCleared 调用)时,viewModelScope 自动取消
// 所有正在运行的协程也会被自动取消,无需任何手动操作
}
viewModelScope 的生命周期与 ViewModel 绑定。当 Activity 旋转屏幕时,Activity 重建但 ViewModel 不销毁,viewModelScope 内的协程也不会被取消——这正是我们想要的行为。
lifecycleScope——UI 层(Activity/Fragment)的首选
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// lifecycleScope 与 Activity 生命周期绑定
// Activity 销毁时,协程自动取消
lifecycleScope.launch {
viewModel.uiState.collect { state ->
render(state)
}
}
}
}
但注意一个细节:lifecycleScope.launch 启动的协程会在 Activity 销毁时取消,但 Activity 进入后台时不会暂停。这意味着即使 UI 不可见,协程仍在运行。
对于 Flow 的收集,更推荐使用 repeatOnLifecycle(后面专门讲)。
GlobalScope——几乎不要用
// 几乎永远不要这样写!
GlobalScope.launch {
fetchData()
}
GlobalScope 的生命周期和整个应用进程一样长,它内部的协程不受任何组件生命周期的约束。这意味着:
- Activity 销毁后,协程还在跑,可能继续持有 Activity 的引用,导致内存泄漏
- 你无法统一取消这些协程
- 无法追踪它们的运行状态
唯一合理使用 GlobalScope 的场景是:你明确希望某个操作在整个应用生命周期内都运行,比如应用启动时的初始化任务。即便如此,也应该先考虑是否有更合适的替代方案。
4.3 自定义 CoroutineScope——Repository/Service 层
有时你需要在不依赖 Android 组件的地方(比如纯 Kotlin 的 Repository 或 Service)管理协程。这时需要自定义作用域:
class DataSyncService {
// 正确方式:创建有明确调度器和 SupervisorJob 的 scope
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun startSync() {
serviceScope.launch {
// 执行同步任务
}
}
fun stopSync() {
// 明确调用 cancel,释放资源
serviceScope.cancel()
}
}
这里用 SupervisorJob 而不是普通 Job 的原因:如果 scope 里某一个子协程失败,不会影响其他子协程(后面会详细讲)。
4.4 coroutineScope 函数——临时作用域
coroutineScope 是一个挂起函数(注意和 CoroutineScope 接口的区别),用于在挂起函数内部创建一个临时的子作用域:
suspend fun loadPageData(): PageData = coroutineScope {
// 在这个临时作用域内并行启动多个请求
val userDeferred = async { userRepository.getUser() }
val postsDeferred = async { postRepository.getPosts() }
// 两个请求都完成后,coroutineScope 才返回
PageData(
user = userDeferred.await(),
posts = postsDeferred.await()
)
}
coroutineScope 的特点:
- 它是一个挂起函数,内部任何子协程未完成时它都会挂起
- 如果任何子协程失败,其他子协程也会被取消,整个
coroutineScope抛出异常 - 它继承外部协程的上下文(调度器等)
实际开发中的经验总结:
| 场景 | 推荐作用域 |
|---|---|
| ViewModel 中的网络请求、数据加载 | viewModelScope |
| Activity/Fragment 中的 UI 操作 | lifecycleScope |
| 挂起函数内部的并行操作 | coroutineScope { } |
| 需要独立生命周期的 Service/Repository | 自定义 CoroutineScope(SupervisorJob() + Dispatchers.IO) |
| 测试代码 | runTest { } |
| 绝对不用 | GlobalScope(除极特殊情况)、runBlocking(除测试/main 函数) |
第五章:结构化并发
5.1 什么是结构化并发?
结构化并发是 Kotlin 协程最精妙的设计之一。它的核心思想是:协程必须在特定的作用域内启动,父协程负责其所有子协程的生命周期。
这个设计解决了异步编程的一个经典难题:如何保证"你启动的任务,你一定能管住它"。
fun main() = runBlocking {
// 父协程
launch {
// 子协程 1
launch {
delay(1000)
println("子协程 1 完成")
}
// 子协程 2
launch {
delay(2000)
println("子协程 2 完成")
}
println("父协程的子协程都启动了")
}
// runBlocking 会等待所有子协程完成才退出
}
输出:
父协程的子协程都启动了
子协程 1 完成
子协程 2 完成
5.2 父子协程的三条规则
规则一:父协程等待所有子协程完成
val parentJob = launch {
launch { delay(1000); println("子协程 A") }
launch { delay(2000); println("子协程 B") }
println("父协程体执行完了,但还在等子协程")
}
parentJob.join()
println("父协程真正完成了")
// 输出顺序:
// 父协程体执行完了,但还在等子协程
// 子协程 A
// 子协程 B
// 父协程真正完成了
规则二:取消父协程,所有子协程都被取消
val parentJob = launch {
val childA = launch {
try {
delay(10000)
} catch (e: CancellationException) {
println("子协程 A 被取消")
}
}
val childB = launch {
try {
delay(10000)
} catch (e: CancellationException) {
println("子协程 B 被取消")
}
}
}
delay(100)
parentJob.cancel() // 取消父协程,A 和 B 都被取消
// 输出:
// 子协程 A 被取消
// 子协程 B 被取消
规则三:子协程异常会传播给父协程(使用默认 Job 时)
val scope = CoroutineScope(Job())
scope.launch {
launch {
throw RuntimeException("子协程出错了!")
// 这个异常会导致父协程也被取消
}
launch {
delay(1000)
println("这行不会被执行,因为兄弟协程失败了")
}
}
第六章:并发实战
6.1 串行 vs 并行——最常被误解的知识点
很多初学者容易犯的错误是:以为协程自动并行。实际上,在同一个协程里顺序调用挂起函数,默认是串行的。
// 串行执行(两个请求一个接一个)
suspend fun loadDataSerially() {
val user = fetchUser() // 耗时 1 秒,完成后才执行下一行
val posts = fetchPosts() // 耗时 1 秒
// 总耗时:约 2 秒
}
// 并行执行(两个请求同时发出)
suspend fun loadDataParallel() = coroutineScope {
val userDeferred = async { fetchUser() } // 立即启动,不等待
val postsDeferred = async { fetchPosts() } // 立即启动,不等待
val user = userDeferred.await() // 等待结果
val posts = postsDeferred.await() // 此时可能已经完成了
// 总耗时:约 1 秒(取最慢的那个)
}
实际开发中的并行模式:
// 同时加载多个独立数据,等全部完成后渲染页面
data class HomePageData(
val banner: List<Banner>,
val articles: List<Article>,
val userInfo: UserInfo
)
suspend fun loadHomePage(): HomePageData = coroutineScope {
val bannerDeferred = async { bannerRepository.getBanners() }
val articlesDeferred = async { articleRepository.getArticles() }
val userDeferred = async { userRepository.getUser() }
HomePageData(
banner = bannerDeferred.await(),
articles = articlesDeferred.await(),
userInfo = userDeferred.await()
)
}
6.2 带超时的请求
// withTimeout:超时抛出 TimeoutCancellationException
suspend fun fetchWithTimeout(): String {
return withTimeout(3000) {
fetchSlowData()
}
}
// withTimeoutOrNull:超时返回 null(更常用,不需要 try-catch)
suspend fun fetchWithTimeoutOrNull(): String? {
return withTimeoutOrNull(3000) {
fetchSlowData()
}
}
// 实际使用
lifecycleScope.launch {
val result = withTimeoutOrNull(5000) {
repository.fetchData()
}
if (result == null) {
showError("请求超时,请检查网络")
} else {
displayData(result)
}
}
6.3 重试机制
// 带重试的网络请求
suspend fun <T> retry(
times: Int = 3,
delay: Long = 1000L,
block: suspend () -> T
): T {
repeat(times - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
if (e is CancellationException) throw e // 取消异常不重试
println("第 ${attempt + 1} 次失败,${delay}ms 后重试")
delay(delay)
}
}
return block() // 最后一次,不捕获异常
}
// 使用
lifecycleScope.launch {
try {
val data = retry(times = 3, delay = 2000) {
repository.fetchData()
}
displayData(data)
} catch (e: Exception) {
showError("重试 3 次后仍然失败:${e.message}")
}
}
第七章:异常处理
7.1 协程的异常传播机制
协程的异常处理和普通代码有所不同,需要特别注意两种构建器的差异。
launch 中的异常:立即传播给父协程。
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
launch {
throw RuntimeException("子协程崩溃了")
// 异常立即传播,导致 scope 内所有协程被取消
}
delay(1000)
println("这行不会执行") // 因为兄弟协程崩溃了
}
async 中的异常:暂存在 Deferred 中,调用 .await() 时才抛出。
val deferred = scope.async {
throw RuntimeException("async 中的异常")
}
// 这里才会抛出异常
try {
val result = deferred.await()
} catch (e: RuntimeException) {
println("捕获到:${e.message}")
}
7.2 CoroutineExceptionHandler
为协程作用域设置全局异常处理器,捕获未被处理的异常:
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
// 可以在这里上报错误到 Sentry/Bugly 等
// 可以显示全局错误提示
println("未处理的协程异常:${throwable.message}")
}
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main + exceptionHandler)
scope.launch {
throw RuntimeException("这个会被 handler 捕获")
}
注意:CoroutineExceptionHandler 只对 launch 启动的根协程有效(即直接在 scope 上 launch,而非在协程内部 launch 的子协程)。对 async 也无效。
在 Android 的 viewModelScope 和 lifecycleScope 中,你可以在 launch 时传入:
viewModelScope.launch(exceptionHandler) {
// 这个协程内未捕获的异常,会被 handler 处理
val data = repository.fetchData()
}
7.3 SupervisorJob——隔离失败
默认情况下,一个子协程失败会导致整个父作用域崩溃,兄弟协程也被取消。这是"普通 Job"的行为。
SupervisorJob 改变了这一行为:某个子协程失败,其他子协程不受影响。
// 普通 Job:任意子协程失败,其他全部取消
val normalScope = CoroutineScope(Job())
normalScope.launch { throw Exception("我失败了") }
normalScope.launch { delay(1000); println("我也被取消了!") }
// SupervisorJob:子协程相互独立
val supervisorScope = CoroutineScope(SupervisorJob())
supervisorScope.launch { throw Exception("我失败了") }
supervisorScope.launch { delay(1000); println("我没事,继续运行") }
实际开发中:viewModelScope 和 lifecycleScope 内部都使用了 SupervisorJob,所以在这两个 scope 里启动的多个协程是相互独立的——一个失败不会影响其他的。
supervisorScope 函数用于在挂起函数内创建类似效果的子作用域:
suspend fun loadMultipleData() = supervisorScope {
val result1 = async { fetchData1() }
val result2 = async { fetchData2() }
val result3 = async { fetchData3() }
// 某个失败,其他继续,分别处理
listOf(result1, result2, result3).mapNotNull { deferred ->
try {
deferred.await()
} catch (e: Exception) {
null // 该请求失败,返回 null,不影响其他
}
}
}
7.4 try-catch 的正确姿势
lifecycleScope.launch {
try {
val result = fetchData()
displayResult(result)
} catch (e: HttpException) {
showHttpError(e.code())
} catch (e: IOException) {
showNetworkError()
} catch (e: CancellationException) {
// 非常重要!CancellationException 不要吞掉,必须重新抛出
// 否则取消机制会失效,协程无法正常被取消
throw e
}
}
或者更简洁:
lifecycleScope.launch {
runCatching { fetchData() }
.onSuccess { displayResult(it) }
.onFailure { e ->
if (e is CancellationException) throw e
showError(e.message)
}
}
第八章:取消与生命周期
8.1 协程取消的工作原理
协程的取消是协作式的——协程需要主动配合才能被取消。这是很多人忽视的一点。
val job = launch {
repeat(1000) { i ->
println("执行第 $i 次")
delay(500) // delay 是挂起点,取消会在这里生效
}
}
delay(2000)
job.cancel() // 请求取消,协程在下一个挂起点(delay)处响应取消
job.join()
8.2 让 CPU 密集型协程可取消
如果协程里没有挂起函数,它就不会响应取消:
// 这个协程无法被取消!
val job = launch(Dispatchers.Default) {
var i = 0
while (i < 1_000_000) {
i++ // 纯计算,没有挂起点
}
}
job.cancel() // 发出取消请求,但协程不响应
解决方案1:检查 isActive
val job = launch(Dispatchers.Default) {
var i = 0
while (i < 1_000_000 && isActive) {
i++
}
}
解决方案2:使用 yield() 主动让出
val job = launch(Dispatchers.Default) {
var i = 0
while (i < 1_000_000) {
i++
if (i % 1000 == 0) yield() // 每 1000 次让出一次,响应取消
}
}
8.3 取消后的资源清理
val job = launch {
try {
doSomeWork()
} finally {
// 协程被取消时,finally 块仍然执行,适合做清理
closeConnection()
println("连接已关闭")
}
}
// 如果 finally 里需要调用挂起函数(默认情况下不允许,因为协程已取消)
val job2 = launch {
try {
doSomeWork()
} finally {
withContext(NonCancellable) {
// NonCancellable 上下文中可以调用挂起函数
saveDataToDatabase() // 确保数据保存完成再退出
}
}
}
第九章:Flow——协程的数据流
9.1 什么是 Flow?
Flow 是协程的异步数据流 API,用于处理连续的多个异步值。可以把它理解为:异步版的 Sequence,或者比 RxJava 更简洁的响应式流。
- 挂起函数:返回单个值(
suspend fun getUser(): User) - Flow:随时间推移发射多个值(
fun userStream(): Flow<User>)
// 创建一个简单的 Flow
fun timerFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(1000)
emit(i) // 发射一个值
}
}
// 收集 Flow
lifecycleScope.launch {
timerFlow().collect { value ->
println("收到:$value")
}
}
// 每隔 1 秒输出一个数字:1, 2, 3, 4, 5
Flow 是冷流(Cold Flow):只有调用 collect 时,Flow 才开始执行。每次 collect 都是一次独立的执行。
9.2 Flow 的常用操作符
flow { emit(1); emit(2); emit(3); emit(4); emit(5) }
.filter { it % 2 == 0 } // 过滤
.map { it * it } // 转换
.take(2) // 只取前两个
.collect { println(it) } // 输出:4 16
实用操作符:
// debounce:防抖,N 毫秒内无新值才发射
// 经典场景:搜索框输入防抖
searchQueryFlow.debounce(500).collect { query ->
search(query)
}
// distinctUntilChanged:相邻重复值只发射一次
// 经典场景:避免相同关键词重复触发搜索
flow.distinctUntilChanged()
// flatMapLatest:只处理最新值,之前未完成的自动取消
// 经典场景:搜索——用户快速输入时,取消上一次搜索,只保留最新的
searchQueryFlow
.debounce(500)
.flatMapLatest { query ->
flow { emit(api.search(query)) }
}
.collect { results -> showResults(results) }
// buffer:让生产者和消费者并发执行,避免背压问题
flow {
for (i in 1..10) {
delay(100)
emit(i)
}
}
.buffer() // 生产者继续生产,不等消费者
.collect {
delay(300) // 消费者慢,但不会拖慢生产者
println(it)
}
// conflate:只保留最新值,跳过消费者来不及处理的中间值
// 经典场景:位置更新、传感器数据,只关心最新状态
locationFlow
.conflate()
.collect { location -> updateMapMarker(location) }
// catch:在 Flow 中处理异常
flow { emit(fetchData()) }
.catch { e -> emit(defaultData) } // 出错时发射默认值
.collect { displayData(it) }
// onEach:执行副作用,不改变数据
flow { emit(fetchData()) }
.onEach { println("即将展示:$it") } // 日志或埋点
.collect { displayData(it) }
// onStart / onCompletion:Flow 开始和结束时的回调
flow { emit(fetchData()) }
.onStart { showLoading() }
.onCompletion { hideLoading() }
.catch { e -> showError(e.message) }
.collect { displayData(it) }
9.3 StateFlow 和 SharedFlow
这两个是热流(Hot Flow):无论是否有人收集,它们都处于活跃状态。
StateFlow:表示状态的 Flow
StateFlow 始终持有一个当前值,适合表示 UI 状态:
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
_count.value++
}
fun decrement() {
_count.update { it - 1 } // update 是线程安全的原子操作
}
}
// Activity 中
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.count.collect { count ->
textView.text = "计数:$count"
}
}
}
StateFlow 的特点:
- 始终有值,新订阅者立即收到当前值
- 相同的值不会重复发射(有去重机制)
- 线程安全
- 适合:UI 状态、配置数据
SharedFlow:表示事件的 Flow
SharedFlow 适合处理一次性事件(导航、弹窗、Toast 等):
class LoginViewModel : ViewModel() {
// replay = 0:新订阅者不会收到历史事件
private val _events = MutableSharedFlow<LoginEvent>()
val events: SharedFlow<LoginEvent> = _events.asSharedFlow()
fun login(username: String, password: String) {
viewModelScope.launch {
val result = repository.login(username, password)
if (result.isSuccess) {
_events.emit(LoginEvent.NavigateToHome)
} else {
_events.emit(LoginEvent.ShowError(result.errorMessage))
}
}
}
}
sealed class LoginEvent {
object NavigateToHome : LoginEvent()
data class ShowError(val message: String) : LoginEvent()
}
// Activity 中
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.events.collect { event ->
when (event) {
is LoginEvent.NavigateToHome -> navigateToHome()
is LoginEvent.ShowError -> showToast(event.message)
}
}
}
}
StateFlow vs SharedFlow 对比:
| 特性 | StateFlow | SharedFlow |
|---|---|---|
| 初始值 | 必须有 | 可以没有 |
| 当前值 | 有 .value 属性 | 无 |
| 新订阅者 | 立即收到当前值 | 根据 replay 参数决定 |
| 相同值 | 不会重复发射 | 总是发射 |
| 适用场景 | UI 状态 | 一次性事件 |
9.4 flowOn:指定上游的执行线程
// flowOn 改变上游操作的调度器,不影响下游(collect 所在的调度器)
fun loadArticles(): Flow<List<Article>> = flow {
val articles = database.getAllArticles() // 在 IO 线程执行
emit(articles)
}
.map { articles ->
articles.filter { it.isPublished } // 也在 IO 线程执行
}
.flowOn(Dispatchers.IO) // flowOn 影响它上面所有的操作
// collect 在主线程(调用者的调度器)
lifecycleScope.launch {
loadArticles().collect { articles ->
adapter.submitList(articles) // 在主线程执行
}
}
第十章:在 Android 中安全地收集 Flow
10.1 repeatOnLifecycle——推荐方式
repeatOnLifecycle 解决了一个重要问题:UI 不可见时不应该收集数据,避免浪费资源或产生副作用。
// 不推荐:即使 App 退到后台,仍在收集
lifecycleScope.launch {
viewModel.uiState.collect { state ->
render(state)
}
}
// 推荐:只在 STARTED 状态时收集,进入后台自动暂停
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
render(state)
}
}
}
repeatOnLifecycle 的工作原理:
- 生命周期进入
STARTED状态 → 启动内部协程,开始收集 - 生命周期进入
STOPPED状态(进入后台)→ 取消内部协程,停止收集 - 再次进入
STARTED状态(回到前台)→ 重新启动协程,重新收集
10.2 同时收集多个 Flow
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// 同时收集多个 Flow
launch {
viewModel.uiState.collect { state -> render(state) }
}
launch {
viewModel.events.collect { event -> handleEvent(event) }
}
}
}
第十一章:Mutex 和 Channel——协程间的通信与同步
11.1 Mutex:协程版的互斥锁
当多个协程访问同一共享资源时,需要同步机制:
val mutex = Mutex()
var sharedCounter = 0
// 多个协程同时修改计数器
coroutineScope {
repeat(100) {
launch {
mutex.withLock {
// 同一时间只有一个协程能进入这里
sharedCounter++
}
}
}
}
println("计数器值:$sharedCounter") // 总是 100,没有竞争条件
11.2 Channel:协程间传递数据
Channel 类似于阻塞队列,用于在协程之间安全地传递数据:
// 生产者-消费者模式
fun main() = runBlocking {
val channel = Channel<Int>()
// 生产者
launch {
for (i in 1..5) {
println("发送:$i")
channel.send(i)
}
channel.close() // 发送完毕,关闭 channel
}
// 消费者
launch {
for (value in channel) {
println("接收:$value")
}
}
}
第十二章:测试协程
12.1 使用 runTest
测试协程使用 runTest(来自 kotlinx-coroutines-test),它会自动跳过 delay,让测试运行得很快:
// build.gradle
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.x")
@Test
fun `测试挂起函数`() = runTest {
// runTest 内部的 delay 会被跳过,测试几乎瞬间完成
val result = repository.getUser()
assertEquals("张三", result.name)
}
@Test
fun `测试 delay 的时间`() = runTest {
var result = ""
launch {
delay(1000)
result = "完成"
}
// 虚拟时间推进 1 秒
advanceTimeBy(1000)
assertEquals("完成", result)
}
12.2 使用 TestDispatcher 测试 ViewModel
class UserViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule() // 替换主线程调度器
private lateinit var viewModel: UserViewModel
private val fakeRepository = FakeUserRepository()
@Before
fun setup() {
viewModel = UserViewModel(fakeRepository)
}
@Test
fun `加载用户数据成功`() = runTest {
viewModel.loadUser()
advanceUntilIdle() // 等待所有协程完成
assertTrue(viewModel.uiState.value is UiState.Success)
}
}
// 测试用的 MainDispatcherRule
class MainDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
第十三章:常见陷阱与最佳实践
13.1 陷阱:在非主线程更新 UI
// 错误:在 IO 线程直接更新 UI,会崩溃
viewModelScope.launch(Dispatchers.IO) {
val data = fetchData()
textView.text = data // CalledFromWrongThreadException!
}
// 正确方式1:在 IO 线程做工作,结果通过 StateFlow 传到主线程
viewModelScope.launch(Dispatchers.IO) {
val data = fetchData()
_uiState.value = UiState.Success(data) // StateFlow 是线程安全的
}
// 正确方式2:用 withContext 切回主线程
viewModelScope.launch(Dispatchers.IO) {
val data = fetchData()
withContext(Dispatchers.Main) {
textView.text = data
}
}
13.2 陷阱:async 的错误用法
// 错误:立即 await,等于串行
suspend fun badParallel() = coroutineScope {
val a = async { fetchA() }.await() // 等 A 完成
val b = async { fetchB() }.await() // 才开始 B
// 完全失去了 async 的意义
}
// 正确:先全部启动,再全部 await
suspend fun goodParallel() = coroutineScope {
val aDeferred = async { fetchA() }
val bDeferred = async { fetchB() }
val a = aDeferred.await()
val b = bDeferred.await()
}
13.3 陷阱:在阻塞代码中使用挂起
// 错误:Thread.sleep 阻塞线程,协程无法取消
suspend fun badSleep() {
Thread.sleep(5000) // 阻塞 5 秒,协程无法响应取消
}
// 正确
suspend fun goodSleep() {
delay(5000) // 挂起 5 秒,可以被取消
}
// 如果必须调用阻塞的第三方 API,要在 IO 调度器上执行
suspend fun callBlockingApi(): String = withContext(Dispatchers.IO) {
someBlockingLibrary.fetch() // 阻塞调用在 IO 线程上是可以的
}
13.4 陷阱:忘记处理 CancellationException
// 错误:吞掉了 CancellationException,取消机制失效
suspend fun badCatch() {
try {
delay(10000)
} catch (e: Exception) {
// 所有异常都被吞掉了,包括 CancellationException
println("出错了:${e.message}")
}
}
// 正确
suspend fun goodCatch() {
try {
delay(10000)
} catch (e: CancellationException) {
throw e // 取消异常必须重新抛出
} catch (e: Exception) {
println("出错了:${e.message}")
}
}
13.5 Repository 层的正确写法
Repository 中的函数应该暴露挂起函数或 Flow,而不是内部自己启动协程:
// 错误:Repository 内部自己启动协程,调用方无法控制生命周期
class BadRepository {
fun loadData() {
GlobalScope.launch { // 生命周期不受控
val data = api.fetchData()
// 怎么把结果传出去?用回调?又回到了回调地狱...
}
}
}
// 正确:暴露挂起函数,让调用方(ViewModel)决定在哪里运行
class GoodRepository(private val api: Api) {
suspend fun loadData(): Data = withContext(Dispatchers.IO) {
api.fetchData()
}
fun dataStream(): Flow<Data> = flow {
while (true) {
emit(api.fetchData())
delay(30_000)
}
}.flowOn(Dispatchers.IO)
}
第十四章:综合实战案例
用一个完整的案例把前面所有内容串起来——带搜索防抖的新闻列表:
// ======== 数据层 ========
data class Article(val id: Int, val title: String)
class NewsRepository(private val api: NewsApi) {
suspend fun search(query: String): List<Article> = withContext(Dispatchers.IO) {
api.searchArticles(query)
}
}
// ======== ViewModel 层 ========
sealed class SearchUiState {
object Idle : SearchUiState()
object Loading : SearchUiState()
data class Success(val articles: List<Article>) : SearchUiState()
data class Error(val message: String) : SearchUiState()
}
class NewsViewModel(private val repository: NewsRepository) : ViewModel() {
private val _uiState = MutableStateFlow<SearchUiState>(SearchUiState.Idle)
val uiState: StateFlow<SearchUiState> = _uiState.asStateFlow()
private val searchQuery = MutableStateFlow("")
init {
viewModelScope.launch {
searchQuery
.debounce(500) // 防抖 500ms
.filter { it.length >= 2 } // 最少 2 个字符
.distinctUntilChanged() // 相同词不重复搜索
.flatMapLatest { query -> // 只关心最新的搜索,旧的自动取消
flow {
emit(SearchUiState.Loading)
try {
val articles = repository.search(query)
emit(SearchUiState.Success(articles))
} catch (e: CancellationException) {
throw e // 取消异常必须重新抛出
} catch (e: Exception) {
emit(SearchUiState.Error(e.message ?: "搜索失败"))
}
}
}
.collect { _uiState.value = it }
}
}
fun onSearchQueryChanged(query: String) {
searchQuery.value = query
}
}
// ======== UI 层 ========
class NewsActivity : AppCompatActivity() {
private val viewModel: NewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
searchEditText.doAfterTextChanged { text ->
viewModel.onSearchQueryChanged(text.toString())
}
// 使用 repeatOnLifecycle 安全收集 Flow
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
when (state) {
is SearchUiState.Idle -> showIdleState()
is SearchUiState.Loading -> showLoading()
is SearchUiState.Success -> {
hideLoading()
adapter.submitList(state.articles)
}
is SearchUiState.Error -> {
hideLoading()
showError(state.message)
}
}
}
}
}
}
}
总结
核心概念速查
基础
- 协程 = 可挂起、可恢复的代码块,运行在线程上但不等同于线程
suspend= 函数有能力挂起,不一定真的挂起launch= 启动无返回值协程,async= 启动有返回值协程runBlocking= 只用于测试和 main 函数,开发中不用
作用域(重点)
viewModelScope= ViewModel 层首选,ViewModel 销毁时自动取消lifecycleScope= UI 层首选,Activity/Fragment 销毁时自动取消coroutineScope { }= 挂起函数内部的临时并行作用域GlobalScope= 不要用- 自定义 scope = 用
SupervisorJob() + Dispatchers.IO
调度器
Dispatchers.Main= UI 操作Dispatchers.IO= 网络、文件、数据库Dispatchers.Default= CPU 密集型计算withContext= 在协程内切换调度器
异常处理
try-catch正常工作,但不要吞掉CancellationExceptionCoroutineExceptionHandler= 全局兜底处理SupervisorJob= 隔离子协程的失败
Flow
flow { emit(...) }= 冷流,每次 collect 才执行StateFlow= 状态,始终有值,适合 UI 状态SharedFlow= 事件,适合一次性事件repeatOnLifecycle= 安全收集的标准方式
学习路径建议
- 先掌握基础:
suspend、launch、async、withContext、调度器 - 理解作用域:
viewModelScope、lifecycleScope的使用时机 - 掌握异常处理:正确使用
try-catch和CancellationException - 学习 Flow:从简单的
flow { }开始,再学StateFlow和SharedFlow - 实战项目:把所有知识应用到一个真实项目中
调试协程时,只需要把三个问题想清楚:这个协程的作用域是什么?它在哪个线程运行?它会在什么时候被取消? 把这三个问题想清楚,大多数问题都能迎刃而解。