第一章:Kotlin协程基础——从概念到实战
协程,说白了就是轻量级的线程。你想想看,线程切换开销大到吓人,而协程的挂起几乎不消耗资源。我在项目中第一次用协程时,以为它就是个语法糖,直到看到内存占用从几百MB降到了几十MB……嗯,从那以后我就再也不敢小看它了。
1.1 协程的核心概念
协程不是线程,它是一种可挂起的计算。你可以挂起一个协程,等某个耗时操作(比如网络请求)完成后恢复它,而不会阻塞当前线程。Kotlin 的协程完全由编译器处理挂起和恢复,你只需要在函数前加一个 suspend 关键字。
关键区别:线程是操作系统调度的,协程是用户态调度的。一个线程可以运行成千上万个协程,这就是“轻量级”的真正含义。
1.2 协程作用域:GlobalScope
GlobalScope 是整个应用级别的协程作用域。它的生命周期与进程一致,应用不退出,它就一直活。
// 不推荐在生产代码中使用
GlobalScope.launch {
delay(1000L)
println("Hello from global scope")
}
我曾经在早期项目里大量使用 GlobalScope,结果协程泄漏导致内存吃紧。你的协程启动后如果无人接管,它可能永远不结束。所以现在我的习惯是:除非写测试或快速验证,否则别碰 GlobalScope。
1.3 阻塞桥接:runBlocking
runBlocking 会阻塞当前线程直到内部协程执行完毕。它通常用在 main 函数或单元测试中,作为普通代码和协程世界的桥梁。
fun main() {
runBlocking {
delay(1000L)
println("runBlocking 里的协程执行完")
}
println("main 线程继续")
}
测试协程代码时,我经常用 runBlocking 替代 Thread.sleep()。但要记住:把它用在 Android 主线程里?那就等着 ANR 吧——这坑我踩过。
1.4 启动协程:launch
launch 会启动一个新协程并立即返回一个 Job 对象。它不等待协程结束,所以属于“fire-and-forget”风格。如果你需要等待,可以用 join()。
runBlocking {
val job = launch {
delay(500L)
println("launch 任务完成")
}
job.join()
println("等待结束后执行")
}
1.5 带返回值的协程:async
async 与 launch 类似,但它返回 Deferred<T>,你可以在需要的时候调用 await() 获取结果。这很适合并行执行多个任务再汇总结果。
runBlocking {
val deferred = async {
delay(1000L)
"结果来了"
}
println("等待结果...")
val result = deferred.await()
println(result)
}
1.6 launch vs async 对比
| 特性 | launch | async |
|---|---|---|
| 返回值 | Job | Deferred<T> |
| 是否等待结果 | 不等待,用 join() | 用 await() 获取 |
| 典型场景 | 日志、通知、无返回值操作 | 网络请求、计算、需要结果的任务 |
| 结构化并发 | 作用域结束后自动取消 | 同样遵守结构化并发 |
1.7 结构化并发原则
协程必须在一个作用域内启动,这样当作用域结束时,所有子协程都会被自动取消。这就是 Kotlin 协程的结构化并发。GlobalScope 之所以危险,就是因为它不属于任何作用域,容易失控。
// 推荐的方式:使用 coroutineScope 或 supervisorScope
suspend fun fetchData() = coroutineScope {
launch { doTask1() }
launch { doTask2() }
}
结构化并发让协程变得可管理、可预测。你只需关心作用域的生命周期,剩下的交给协程框架。
我个人习惯在测试中使用
runTest 替代 runBlocking,它更智能,能自动处理延时。但别急,后续章节会专门讲测试,这里先打好基础。
好了,第一章就这些。协程的概念其实不复杂——轻量、可挂起、结构化。后面我们会把这些基础运用到真正的异步测试中,你会发现,掌握这些,Flow 什么的也就水到渠成了。