5、协程与高阶函数:suspend函数、async/await、Flow的创建与操作、高阶函数在协程中的应用、协程上下文与调度器
协程这东西,刚接触时觉得玄乎,用久了你会发现它其实就是一套「能把异步代码写得像同步一样」的框架。我个人习惯把协程理解为「轻量级线程」——它不绑定操作系统线程,可以在线程间随意跳转,挂起和恢复的成本极低。
今天我们就来聊聊协程和高阶函数怎么配合,以及那些你天天用但可能没深想过的概念。
suspend函数:协程的基本单元
suspend 函数是协程的基石。说白了,就是一个可以被挂起、稍后再恢复的函数。挂起时不会阻塞线程,这点和回调不一样。
suspend fun fetchUserData(): User {
// 模拟网络请求
delay(1000)
return User("张三")
}
我在项目中遇到过一个问题:新手容易在 suspend 函数里直接调用另一个 suspend 函数,却忘了它只能在协程作用域内执行。嗯,这里要注意——suspend 函数必须在协程里才能跑,否则编译都过不去。
核心要点:suspend 函数可以调用其他 suspend 函数,但不能在普通函数里直接调用 suspend 函数。你需要用 GlobalScope.launch、runBlocking 或者 lifecycleScope 来启动协程。
async/await:并行任务的利器
async 和 await 是 Kotlin 协程里我最喜欢的组合。async 启动一个协程并返回 Deferred 对象,await 等待结果。两个任务可以并行执行,而不是串行等待。
coroutineScope {
val user = async { fetchUser() }
val posts = async { fetchPosts() }
// 两个请求并行,都完成后才继续
val result = Pair(user.await(), posts.await())
}
我曾经在做一个首页聚合接口时,需要同时拉取用户信息、推荐列表和广告数据。如果用回调嵌套,代码会变成「回调地狱」。用 async/await 后,三个请求并行,代码却像同步一样清晰。
小技巧:如果你有多个不依赖彼此的网络请求,一定要用 async 并行执行。串行的话,总耗时是各请求耗时之和;并行的话,总耗时是最慢的那个请求的耗时。
Flow:响应式数据流
Flow 是 Kotlin 协程里的响应式数据流,类似于 RxJava 的 Observable,但更轻量、更自然。它适合处理那些会多次发射数据的情景,比如数据库监听、传感器数据、下载进度等。
创建 Flow
fun countDown(): Flow<Int> = flow {
for (i in 10 downTo 0) {
emit(i)
delay(1000)
}
}
操作 Flow
Flow 的操作符和高阶函数密不可分。map、filter、catch 这些本质上都是高阶函数——它们接收 lambda 作为参数,对数据流进行变换。
countDown()
.filter { it % 2 == 0 } // 只保留偶数
.map { "剩余 $it 秒" }
.catch { e -> emit("出错了: ${e.message}") }
.collect { println(it) }
你想想看,如果没有高阶函数,这些操作得写多少样板代码?Flow 的设计哲学就是「用高阶函数组合数据流」,这和函数式编程的思路一脉相承。
避坑指南:我曾经在 collect 里直接做了耗时操作,导致 UI 卡顿。记住,collect 默认运行在调用它的协程上下文中。如果你在 Android 主线程 collect,里面就别做耗时操作。用 flowOn 切换调度器可以解决这个问题。
高阶函数在协程中的应用
协程和高阶函数的结合,远不止 Flow 操作符这么简单。我们来看几个典型场景。
自定义协程构建器
你可以用高阶函数封装协程的启动逻辑。比如封装一个带超时控制的网络请求:
suspend fun <T> withTimeoutRequest(
timeout: Long,
block: suspend () -> T
): T {
return withTimeout(timeout) {
block()
}
}
// 使用
val result = withTimeoutRequest(5000) {
fetchData()
}
这里 block 就是一个 suspend lambda,是高阶函数的典型用法。我经常用这种方式封装一些通用逻辑,比如重试、缓存、超时等。
协程作用域函数
coroutineScope、withContext、supervisorScope 这些函数,本质上都是高阶函数。它们接收一个 suspend lambda,并在特定的上下文中执行。
suspend fun loadData(): Data = coroutineScope {
val part1 = async { loadPart1() }
val part2 = async { loadPart2() }
Data(part1.await(), part2.await())
}
coroutineScope 会等待所有子协程完成,并且如果任何一个子协程抛出异常,整个作用域都会取消。这就是结构化并发的核心。
协程上下文与调度器
协程上下文是协程运行时的环境信息集合。它包含调度器、异常处理器、协程名称等元素。调度器决定了协程在哪个线程上执行。
| 调度器 | 用途 | 示例场景 |
|---|---|---|
| Dispatchers.Main | 主线程,用于 UI 操作 | 更新 TextView、显示 Dialog |
| Dispatchers.IO | IO 密集型任务 | 网络请求、数据库读写、文件操作 |
| Dispatchers.Default | CPU 密集型任务 | 图片压缩、数据解析、复杂计算 |
| Dispatchers.Unconfined | 不限制线程(不推荐日常使用) | 测试或特殊场景 |
我在项目中经常用 withContext 切换调度器。比如从数据库读取数据后,切回主线程更新 UI:
suspend fun loadAndShow() {
val data = withContext(Dispatchers.IO) {
database.loadData()
}
// 自动回到 Dispatchers.Main
textView.text = data.toString()
}
为什么会这样?因为 withContext 会继承外部的协程上下文,但用指定的调度器替换原来的。如果你在 lifecycleScope.launch(Dispatchers.Main) 里调用 withContext(Dispatchers.IO),执行完 IO 任务后会自动回到 Main。
记住一条原则:不要在主线程做耗时操作,也不要在 IO 线程更新 UI。用 withContext 或 flowOn 明确指定调度器,代码的可读性和安全性都会提升。
知识体系总览
下面这张图总结了协程与高阶函数的核心关系:
这张图展示了协程的四个核心概念以及它们之间的关系。suspend 函数是基础,async/await 解决并行问题,Flow 处理数据流,上下文和调度器控制执行环境。而高阶函数就像胶水一样,把这些概念粘合在一起。
我个人觉得,协程最难的不是语法,而是思维方式的转变。从回调思维切换到协程思维,需要一点时间。但一旦你习惯了,就会觉得「回不去了」——协程配合高阶函数,写出来的代码既简洁又健壮。
最后分享一个经验:刚开始用协程时,别想着一步到位。先从 launch 和 delay 开始,慢慢过渡到 async/await,再尝试 Flow。每一步都亲手写几个例子,比看十遍文档都管用。
公众号:蓝海资料掘金营,微信deep3321