第3章:Kotlin协程入门:GlobalScope、runBlocking、suspend函数与Job
协程这东西,刚接触时确实有点绕。我记得第一次看协程代码,满脑子都是「这玩意儿到底怎么跑的?」。别急,咱们一步步来拆解。
说白了,协程就是一套线程框架。但它比线程轻量得多。你想想看,开一万个线程能把机器搞崩,但开一万个协程?小意思。
3.1 GlobalScope:全局作用域
GlobalScope 是协程的「全局大管家」。它的生命周期跟整个应用绑定。应用不退出,它就不结束。
// 不推荐这样写
GlobalScope.launch {
delay(1000)
println("我跑在全局作用域里")
}
// 更好的做法:用自定义作用域
class MyViewModel : ViewModel() {
private val scope = CoroutineScope(Dispatchers.Main)
fun doSomething() {
scope.launch {
// 安全地执行协程
}
}
}
我在项目中遇到过一个问题:用 GlobalScope 启动了一个网络请求,用户退出了页面,请求还在跑。结果回调时 UI 已经销毁了,直接崩溃。嗯,从那以后我再也不敢随便用 GlobalScope 了。
3.2 runBlocking:阻塞的桥梁
runBlocking 是协程世界和普通世界之间的「桥梁」。它会阻塞当前线程,直到协程执行完毕。
fun main() {
println("开始:${Thread.currentThread().name}")
runBlocking {
delay(1000)
println("协程内:${Thread.currentThread().name}")
}
println("结束:${Thread.currentThread().name}")
}
// 输出:
// 开始:main
// 协程内:main
// 结束:main
你发现没?整个代码都在主线程执行,但中间有 1 秒的延迟。这就是 runBlocking 的「阻塞魔法」。
3.3 suspend函数:协程的「暂停键」
suspend 是协程最核心的关键字。加了 suspend 的函数,可以在不阻塞线程的情况下暂停执行。
为什么会这样?因为 suspend 函数背后有个「续体传递风格」(CPS)的编译魔法。编译器会把 suspend 函数改造成一个状态机。
// 普通函数
fun fetchUser(): User {
// 这里会阻塞线程
Thread.sleep(1000)
return api.getUser()
}
// suspend 函数
suspend fun fetchUserSuspend(): User {
// 这里不会阻塞线程
delay(1000)
return api.getUser()
}
我刚开始学的时候,总觉得 suspend 函数和普通函数没啥区别。直到我看了反编译后的字节码——好家伙,一个简单的 suspend 函数被拆成了好几个状态。
| 特性 | 普通函数 | suspend函数 |
|---|---|---|
| 线程阻塞 | 会阻塞 | 不会阻塞 |
| 调用限制 | 任何地方 | 只能在协程或另一个suspend函数中 |
| 编译后 | 普通方法 | 状态机 + Continuation |
3.4 Job:协程的「身份证」
每个协程启动后,都会返回一个 Job 对象。它就是协程的「身份证」,用来控制协程的生命周期。
val job: Job = GlobalScope.launch {
delay(1000)
println("任务执行中")
}
// 等待协程完成
job.join()
// 取消协程
job.cancel()
// 检查状态
println("是否活跃:${job.isActive}")
println("是否完成:${job.isCompleted}")
println("是否取消:${job.isCancelled}")
Job 有几个关键状态:New、Active、Completing、Completed、Cancelling、Cancelled。嗯,看着挺多,但实际开发中你只需要关心三个:isActive、isCompleted、isCancelled。
class MyViewModel : ViewModel() {
private val job = Job()
private val scope = CoroutineScope(Dispatchers.IO + job)
fun loadData() {
scope.launch {
// 网络请求
}
}
override fun onCleared() {
super.onCleared()
job.cancel() // 页面销毁时取消所有协程
}
}
3.5 综合实战:一个完整的例子
咱们把今天学的串起来,写个完整的例子:
import kotlinx.coroutines.*
fun main() = runBlocking {
println("主程序开始")
// 启动一个协程
val job = launch {
println("协程1开始")
delay(1000)
println("协程1结束")
}
// 启动另一个协程
val job2 = launch {
println("协程2开始")
delay(500)
println("协程2结束")
}
// 等待两个协程完成
job.join()
job2.join()
println("主程序结束")
}
// 输出:
// 主程序开始
// 协程1开始
// 协程2开始
// 协程2结束
// 协程1结束
// 主程序结束
你发现没?协程2虽然后启动,但因为它只延迟了500ms,所以先结束。这就是协程的「非阻塞并发」——两个任务在同一个线程里交替执行,但谁都不阻塞谁。
- GlobalScope:全局作用域,慎用
- runBlocking:阻塞桥梁,用于main和测试
- suspend:暂停函数,不阻塞线程
- Job:协程控制器,管理生命周期