81、Kotlin协程入门:GlobalScope、launch、async、withContext、Dispatchers
协程这东西,刚接触时觉得玄乎,用久了会发现——它其实就是帮你把异步代码写得像同步一样自然。我最早在项目里引入协程时,团队里还有人质疑:「这不就是线程池换了个马甲吗?」后来大家真香了。
今天咱们就把协程的这几个核心概念掰开揉碎。你跟着我走一遍,保证能上手。
协程是什么?一句话说清楚
协程是一种轻量级的并发框架。轻量到什么程度?一个线程里可以跑成千上万个协程,而线程切换的开销几乎可以忽略。
说白了,协程就是帮你管理「什么时候该等,什么时候该继续」的工具。你不需要手动写回调,不需要担心线程切换,只需要告诉它:「我要做这件事,做完再干下一件。」
核心思想:协程让异步代码看起来像同步代码,但不会阻塞线程。
GlobalScope:全局作用域,慎用
GlobalScope 是协程的顶级作用域。它的生命周期跟整个应用一样长。什么意思?就是你启动了一个协程,如果不手动取消,它会一直运行到应用进程结束。
我在项目里见过有人图省事,到处用 GlobalScope.launch 做网络请求。结果用户退出页面了,请求还在跑,甚至回调里还在更新已经销毁的 UI——直接崩了。
注意:GlobalScope 不适合在 Android 的 Activity、Fragment 或 ViewModel 中使用。它的生命周期不受组件控制,容易造成内存泄漏。
那什么时候用?比如你需要在应用启动时做一些全局初始化,或者后台日志上传,这些跟 UI 无关的任务,可以考虑用 GlobalScope。
launch:点火发射,不关心结果
launch 是最常用的协程构建器。它启动一个协程,不返回任何结果。适合「只管做,不管返回值」的场景。
GlobalScope.launch {
// 模拟耗时操作
delay(1000)
Log.d("TAG", "任务完成")
}
注意,delay() 是协程里的「睡眠」,它不会阻塞线程。这一点跟 Thread.sleep() 有本质区别。你想想看,如果阻塞了线程,那这个线程上的其他协程也得等着,多浪费。
实际项目中,我一般不会直接用 GlobalScope.launch,而是用 viewModelScope.launch 或 lifecycleScope.launch。它们能自动跟随组件的生命周期取消,省心很多。
async:要结果?用这个
如果你需要协程执行完返回一个结果,那就用 async。它返回一个 Deferred 对象,你可以通过 await() 拿到结果。
val deferred = GlobalScope.async {
delay(1000)
"Hello from async"
}
// 在另一个协程中获取结果
GlobalScope.launch {
val result = deferred.await()
Log.d("TAG", result)
}
这里有个坑:await() 是挂起函数,必须在协程里调用。你不能在普通函数里直接 deferred.await(),会报错。
我曾经在项目里用 async 同时发起两个网络请求,然后等两个结果都回来再合并展示。代码写起来特别清爽,比回调嵌套舒服太多了。
小技巧:如果你需要并发执行多个任务,可以用 async 启动多个协程,然后统一 await。这样总耗时取决于最慢的那个任务,而不是串行累加。
withContext:切换线程的利器
withContext 的作用是:在指定调度器上执行代码块,并返回结果。它不会创建新的协程,而是挂起当前协程,切换到指定线程执行,执行完再切回来。
GlobalScope.launch(Dispatchers.Main) {
// 主线程:更新 UI
val result = withContext(Dispatchers.IO) {
// IO 线程:做网络请求或数据库操作
fetchDataFromNetwork()
}
// 回到主线程:更新 UI
textView.text = result
}
你看,代码从上往下读,逻辑是线性的。但实际上,withContext 内部切换到了 IO 线程,执行完又切回主线程。这种写法比回调或 RxJava 的链式调用直观得多。
我个人习惯把 withContext 封装在 Repository 层里。这样 ViewModel 调用时,不需要关心线程切换,直接拿到结果就行。
Dispatchers:调度器,决定协程跑在哪个线程
调度器就是协程的「交通指挥员」。它告诉协程:你该在哪个线程池里跑。
| 调度器 | 用途 | 示例场景 |
|---|---|---|
Dispatchers.Main |
主线程,用于 UI 操作 | 更新 TextView、显示 Dialog |
Dispatchers.IO |
IO 密集型任务 | 网络请求、文件读写、数据库操作 |
Dispatchers.Default |
CPU 密集型任务 | 排序、解析 JSON、图像处理 |
Dispatchers.Unconfined |
不指定线程,随当前线程执行 | 极少使用,一般用于测试 |
嗯,这里要注意:Dispatchers.Main 在 Android 上就是主线程。如果你在后台线程里更新 UI,会直接崩溃。所以一定要确保 UI 操作在 Main 调度器上执行。
我见过一个新手同事,在 Dispatchers.IO 里直接调了 textView.text = ...,结果 app 闪退。排查了半天才发现是线程问题。
知识体系总览
下面这张图帮你理清协程入门阶段的核心概念和关系:
避坑指南:我曾经踩过的坑
最后分享几个我实际项目中遇到的坑,你遇到了能少走弯路:
- GlobalScope 滥用:我曾经在一个 Fragment 里用 GlobalScope 启动协程做网络请求,结果用户快速返回再进入,协程还在跑,回调里更新了旧的 UI 引用——直接空指针。后来全部换成
lifecycleScope,问题解决。 - async 忘记 await:用 async 启动协程后,如果不调用
await(),异常会被静默吞掉。你根本不知道任务失败了。所以记得要么 await,要么用start明确表示不关心结果。 - withContext 嵌套过多:有人喜欢在 withContext 里再套 withContext,其实没必要。一个 withContext 就能切换到指定线程,执行完自动回来。嵌套只会让代码变乱。
- Dispatchers.Main 做耗时操作:这个不用多说,主线程做耗时操作会卡 UI,甚至 ANR。记住:网络请求、数据库读写、文件操作,统统丢给 IO 调度器。
协程入门其实不难,关键是理解这几个核心概念怎么配合。你只要记住:launch 点火,async 要结果,withContext 切线程,Dispatchers 定位置。剩下的就是多写多练了。
公众号:蓝海资料掘金营,微信deep3321