Kotlin协程启动方式详解

引言

协程(Coroutine)是Kotlin在异步编程领域的革命性解决方案。相比传统的线程和回调,协程通过挂起(suspend)而非阻塞的方式,让我们能够以同步代码的写法 实现异步任务的处理 。然而,很多初学者在刚接触协程时,往往被各种启动方式搞得晕头转向:launchasyncrunBlockingwithContext...它们之间有什么区别?分别在什么场景下使用?

本文将用最通俗的语言最直观的代码对比,帮你彻底搞懂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>,包含可等待的结果。asynclaunch的"超集",增加了返回结果的能力。

如何选择使用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 内部的代码都是在当前线程空闲后才执行的,而不是立即执行

因此首先是代码的执行顺序是

  1. 先println("1")

  2. 提交任务launch(叫他A协程)

  3. 执行下一条指令也就是 println(4)

  4. 提交任务async(叫他协程B)

  5. 执行的.await()方法, 线程空闲,触发调用协程

  6. 协程有2个,A,B都可能先执行,因此可能先 println("5"), 也可能先println("2")

这一步是理论上A,B谁都可能先执行,但是实际测试了100000次, 我的手机都是必定先执行打印2, 后打印5。 有知道细节的可以继续讨论一下。

  1. 后续输出6, 7 , 3

请重点记住,启动协程只是放到等待队列,等线程空闲时才执行

相关推荐
Kapaseker19 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴19 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭1 天前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab1 天前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农2 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
鹏程十八少2 天前
4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理
android·前端·面试
Kapaseker2 天前
一杯美式搞定 Kotlin 空安全
android·kotlin
三少爷的鞋2 天前
Android 协程时代,Handler 应该退休了吗?
android
火柴就是我2 天前
让我们实现一个更好看的内部阴影按钮
android·flutter