引言
协程(Coroutine)是Kotlin在异步编程领域的革命性解决方案。相比传统的线程和回调,协程通过挂起(suspend)而非阻塞的方式,让我们能够以同步代码的写法 实现异步任务的处理 。然而,很多初学者在刚接触协程时,往往被各种启动方式搞得晕头转向:launch、async、runBlocking、withContext...它们之间有什么区别?分别在什么场景下使用?
本文将用最通俗的语言 、最直观的代码对比,帮你彻底搞懂Kotlin协程的启动方式和使用场景。
1. 前置准备
在开始之前,请确保项目中已经引入协程依赖:
// build.gradle.kts (Module)
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
2. 协程构建器总览
Kotlin协程主要通过以下几种构建器启动:
| 构建器 | 返回值 | 特点 | 常用场景 |
|---|---|---|---|
launch |
Job |
无返回值,即发即用 | 执行耗时任务,不关心结果 |
async |
Deferred<T> |
有返回值,需要await()获取 |
并行任务,需要结果 |
runBlocking |
T |
阻塞当前线程 | 桥接普通函数和挂起函数,测试 |
withContext |
T |
切换协程上下文 | 指定代码块在特定线程执行 |
下面我们逐个击破。
3. launch:最常用的启动方式
launch是最基础的协程构建器,它返回一个Job对象,不包含任何结果值。
基本用法
fun main() {
// 需要在协程作用域中调用launch
runBlocking {
val job: Job = launch {
delay(1000)
println("任务执行中...")
}
println("launch不会阻塞后续代码")
job.join() // 等待协程执行完毕
println("任务结束")
}
}
输出:
launch不会阻塞后续代码 任务执行中... 任务结束
在Android中的真实场景
class MainActivity : AppCompatActivity() {
private val mainScope = MainScope()
fun loadData() {
// 在UI线程启动协程
mainScope.launch {
showLoading() // UI操作
// 切换到IO线程执行网络请求
val result = withContext(Dispatchers.IO) {
api.fetchData() // 挂起函数,非阻塞
}
hideLoading() // UI操作
showResult(result)
}
}
override fun onDestroy() {
super.onDestroy()
mainScope.cancel() // 防止内存泄漏
}
}
核心特点 :不关心返回值,类似于线程的Thread.start()。
4. async:需要返回值时使用
当我们需要协程执行完毕后返回某个结果时,async是我们的不二选择。它返回Deferred<T>对象,通过await()获取结果。
基本对比
fun main() = runBlocking {
// ========== launch 版本 ==========
var result1 = ""
val job1 = launch {
delay(1000)
result1 = "数据1"
}
job1.join()
println("launch结果: $result1")
// ========== async 版本 ==========
val deferred: Deferred<String> = async {
delay(1000)
return@async "数据1" // 或直接写 "数据1"
}
val result2 = deferred.await()
println("async结果: $result2")
}
输出完全相同 ,但async的代码更加简洁优雅。
并行任务的王者
async的真正威力体现在同时执行多个独立任务:
fun main() = runBlocking {
val time = measureTimeMillis {
// 顺序执行 - 耗时2秒
val result1 = getData1()
val result2 = getData2()
println("顺序结果: $result1 + $result2")
}
println("顺序耗时: ${time}ms")
val time2 = measureTimeMillis {
// 并行执行 - 耗时1秒
val deferred1 = async { getData1() }
val deferred2 = async { getData2() }
val result1 = deferred1.await()
val result2 = deferred2.await()
println("并行结果: $result1 + $result2")
}
println("并行耗时: ${time2}ms")
}
suspend fun getData1(): String {
delay(1000)
return "数据1"
}
suspend fun getData2(): String {
delay(1000)
return "数据2"
}
输出:
顺序结果: 数据1 + 数据2 顺序耗时: 2012ms 并行结果: 数据1 + 数据2 并行耗时: 1015ms
结论 :当任务相互独立时,使用async可以将总耗时从"串行之和"变为"最慢任务的耗时"。
5. runBlocking:桥梁与测试工具
runBlocking是一个特殊的构建器,它会阻塞当前线程直到内部协程执行完毕。
认识runBlocking
fun main() {
println("主线程开始: ${Thread.currentThread().name}")
runBlocking {
println("runBlocking进入: ${Thread.currentThread().name}")
delay(1000)
println("runBlocking执行中...")
}
println("主线程结束: ${Thread.currentThread().name}")
}
输出:
主线程开始: main runBlocking进入: main runBlocking执行中... 主线程结束: main
看到问题了吗?runBlocking内部运行在调用它的线程 ,并且会阻塞该线程。
为什么说它主要是测试工具?
// ❌ 不推荐在正式代码中使用
class MainActivity : AppCompatActivity() {
fun badExample() {
// 这会阻塞UI线程!导致ANR
runBlocking {
delay(5000) // UI线程卡死5秒
}
}
}
// ✅ 只应该在测试或main函数中使用
@Test
fun testCoroutine() = runBlocking {
val result = mySuspendFunction()
assertEquals(expected, result)
}
6. withContext:优雅地切换线程
withContext不会创建新的协程,而是在当前协程内切换上下文(通常是切换线程),并返回结果
与async的对比
fun main() = runBlocking {
// ========== async + await ==========
val result1 = async(Dispatchers.IO) {
fetchUserInfo()
}.await()
// ========== withContext ==========
val result2 = withContext(Dispatchers.IO) {
fetchUserInfo()
}
// 完全等价!但withContext更简洁
println("$result1, $result2")
}
suspend fun fetchUserInfo(): String {
delay(1000)
return "用户信息"
}
Android中的标准模板
class UserViewModel : ViewModel() {
private val _userLiveData = MutableLiveData<User>()
val userLiveData: LiveData<User> = _userLiveData
fun loadUser(userId: String) {
viewModelScope.launch {
_userLiveData.value = UserLoadingState // 显示加载中
// withContext自动切换线程并返回结果
val user = withContext(Dispatchers.IO) {
repository.getUserFromNetwork(userId)
}
_userLiveData.value = user // 回到主线程更新UI
}
}
}
一句话总结 :withContext = 切换线程 + 同步返回结果。
7. 完整对比:所有启动方式同台竞技
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
fun main() = runBlocking {
println("========== 协程启动方式大对比 ==========\n")
// 1. launch
launch {
delay(500)
println("[launch] 无返回值,执行耗时任务")
}
// 2. async
val deferred = async {
delay(500)
println("[async] 执行并返回结果")
"我是返回值"
}
println("[async] 获取结果: ${deferred.await()}")
// 3. withContext
val result = withContext(Dispatchers.IO) {
delay(500)
println("[withContext] 切换线程并返回结果")
"线程: ${Thread.currentThread().name}"
}
println("[withContext] 结果: $result")
// 4. runBlocking 嵌套 - 不推荐
runBlocking {
delay(500)
println("[runBlocking] 会阻塞外层协程的线程")
}
println("\n========== 并行执行能力对比 ==========")
// 串行执行
val time1 = measureTimeMillis {
val user = getUser()
val order = getOrder()
println("串行结果: $user, $order")
}
// async并行
val time2 = measureTimeMillis {
val user = async { getUser() }
val order = async { getOrder() }
println("并行结果: ${user.await()}, ${order.await()}")
}
println("串行耗时: $time1 ms")
println("并行耗时: $time2 ms")
}
suspend fun getUser(): String {
delay(1000)
return "张三"
}
suspend fun getOrder(): String {
delay(1000)
return "订单#10086"
}
8. 其他
launch和async的根本区别是什么?
launch返回Job,不包含结果;async返回Deferred<T>,包含可等待的结果。async是launch的"超集",增加了返回结果的能力。
如何选择使用launch还是async 以及withContext
-
不需要结果 :
launch(如日志上报、数据缓存) -
需要结果 :
async(如网络请求、数据库查询) -
需要切换线程并返回结果 :
withContext(Android开发中的最优解)
以下代码执行顺序是什么?
runBlocking {
println("1")
launch {
println("2")
delay(1000)
println("3")
}
println("4")
async {
println("5")
delay(500)
println("6")
}.await()
println("7")
}
答案:1, 4, 2, 5, 6, 7,3 , 也可能是1, 4, 5, 2, 6, 7 3
(思考:这个才是重点)
答案解释
后面的6, 7,3 可能大家都可能会理解。
很多人认为可能出现【 1, 2, 4, 5】,【1, 2, 5, 4】
主要是协程调度器的工作机制
-
launch只是提交任务 ,不是执行任务 -
提交任务 ≈ 把任务放到待执行队列
-
执行任务 ≈ 从队列取出并运行
-
println("4")在提交任务后立即执行,而此时任务还在队列中等待调度。 -
所有的
launch内部的代码都是在当前线程空闲后才执行的,而不是立即执行
因此首先是代码的执行顺序是
先println("1")
提交任务launch(叫他A协程)
执行下一条指令也就是 println(4)
提交任务async(叫他协程B)
执行的.await()方法, 线程空闲,触发调用协程
协程有2个,A,B都可能先执行,因此可能先 println("5"), 也可能先println("2")
这一步是理论上A,B谁都可能先执行,但是实际测试了100000次, 我的手机都是必定先执行打印2, 后打印5。 有知道细节的可以继续讨论一下。
- 后续输出6, 7 , 3
请重点记住,启动协程只是放到等待队列,等线程空闲时才执行