Kotlin 协程(Coroutines) 是一种轻量级的并发编程方案,专为简化异步编程和复杂并发操作而设计。例如 网络请求、数据库查询、大量计算 等。相比 Java 传统的多线程,协程更加高效、可控、易读。在 Android 开发中,协程能有效解决传统异步编程(如 AsyncTaskHandler、回调嵌套)的痛点,使代码更简洁、可读性更高,同时避免内存泄漏和线程阻塞问题。

线程是非常重量级的,它需要依靠操作系统的调度才能实现不同线程之间的切换,协程却可以仅在编程语言的层面就能实现不同协程之间的切换,从而大大提升了并发编程的运行效率。

什么是协程?

协程本质上是可暂停和恢复的任务,它不会阻塞线程,而是挂起(suspend),等到合适的时机再继续执行。

👉 你可以把它理解为 “改进版的线程”,但比线程更轻量、更灵活。

🔹 特点:

  • 轻量级:多个协程可在单个线程上运行,避免了线程的创建和销毁的开销。
  • 非阻塞:任务不会阻塞线程,而是挂起等待,从而提高资源利用率。
  • 结构化并发:Kotlin 提供了作用域(CoroutineScope),确保协程不会泄漏,且任务可以被有序管理。
  • 简洁代码:相比传统回调(Callback),协程代码更加直观,避免 “回调地狱”。

启动协程

Kotlin 协程通过launchasync启动:

import kotlinx.coroutines.*
 
fun main() = runBlocking {  // 创建一个协程作用域
    launch {
        delay(1000L)  // 挂起 1 秒(不会阻塞线程)
        println("Kotlin 协程")
    }
    println("Hello,")
}

🔹 输出:

Hello,
Kotlin 协程
  • runBlocking {}:创建主协程,阻塞主线程,直到内部协程执行完毕。
  • launch {}:创建一个新的协程,并在后台运行。
  • delay(1000L):挂起当前协程 1 秒,但不会阻塞主线程。

挂起函数(suspend)

Kotlin 提供 suspend 关键字,使得普通函数变成可挂起的异步函数,可以在不阻塞当前线程的情况下暂停协程的执行。:

suspend fun fetchData(): String {
    delay(2000L)  // 模拟耗时任务
    return "数据加载完成"
}
 
fun main() = runBlocking {
    println("开始请求数据...")
    val result = fetchData()  // 调用挂起函数
    println(result)
}

🔹 输出:

开始请求数据...
(等待 2 秒)
数据加载完成

注意:

  • suspend不能单独使用,只能在另一个suspend或协程作用域中调用。

suspendCoroutine

suspendCoroutine 是 Kotlin 协程提供的一个桥接函数,用于将传统的回调式异步代码转换为挂起函数。

挂起(Suspend)的完整生命周期

  1. 触发挂起:当协程执行到suspendCoroutine时,立即暂停当前协程(非阻塞线程)
  2. 释放线程:原线程(如主线程)可继续执行其他任务(如渲染UI、处理点击事件)
  3. 等待回调:异步操作在后台执行(网络请求、数据库查询等)
  4. 恢复执行:通过continuation.resume()唤醒协程,从挂起点继续执行
    例子:
suspend fun <T> Call<T>.await(): T = suspendCoroutine { continuation ->
    enqueue(object : Callback<T> {
        override fun onResponse(call: Call<T>, response: Response<T>) {
            response.body()?.let  { continuation.resume(it)  }  // 成功恢复 
            ?: continuation.resumeWithException(...)       // 空数据异常 
        }
 
        override fun onFailure(call: Call<T>, t: Throwable) {
            continuation.resumeWithException(t)                // 失败传递 
        }
    })
}

核心优势

特点说明
不阻塞线程挂起期间线程可以处理其他任务
代码同步写法看起来像同步代码,实际是异步执行
回调地狱消失不需要嵌套回调,代码更清晰

launch vs async

Kotlin 提供了两种方式来创建协程:

方式作用
launch启动一个协程,不会返回结果,适用于 执行任务
async启动一个协程,返回 Deferred<T>,适用于 计算并获取结果

示例:launch(不返回值)

launch {
    delay(1000L)
    println("使用 launch 启动的协程")
}

示例:async(返回结果)

val deferred = async {
    delay(1000L)
    "Hello, Kotlin"  // 返回值
}
println(deferred.await())  // 等待结果

async 返回 Deferred<T>,必须调用 await() 获取值。


协程作用域(CoroutineScope)

Kotlin 提供了多个协程作用域:

作用域作用
GlobalScope.launch全局协程,生命周期与应用相同,不推荐使用
runBlocking阻塞当前线程,通常用于测试或 main 函数
CoroutineScope推荐方式,提供结构化并发,管理协程生命周期

**示例:使用 **CoroutineScope

class MyRepository {
    // 关键点 ①:创建自定义协程作用域,指定 IO 线程池
    private val scope = CoroutineScope(Dispatchers.IO)
 
    fun fetchData() {
        // 关键点 ②:在 scope 内启动协程
        scope.launch {
            val data = apiRequest() // 挂起函数
            println(data)
        }
    }
 
    // 关键点 ③:模拟网络请求的挂起函数
    suspend fun apiRequest(): String {
        delay(2000L) // 模拟耗时
        return "数据加载完成"
    }
}

代码解释

🔑 ‌关键点 ①:CoroutineScope(Dispatchers.IO)

  • 作用‌:创建一个独立的协程作用域,所有在此作用域内启动的协程共享同一个生命周期。
  • Dispatchers.IO‌:专门用于执行 I/O 密集型任务(如网络请求、文件读写),底层使用线程池优化。

对比‌:如果使用 GlobalScope,协程生命周期与整个应用绑定,容易泄漏。

🔑 ‌关键点 ②:scope.launch

  • 功能‌:在指定作用域内启动一个协程,自动继承 scope 的调度器和生命周期。
  • 优势‌:当调用 scope.cancel() 时,所有通过该作用域启动的协程都会被自动取消。

🔑 ‌关键点 ③:suspend fun apiRequest()

  • 挂起函数‌:用 suspend 修饰,可在协程内暂停执行(如 delay)而不阻塞线程。
  • 线程切换‌:apiRequest 运行在 Dispatchers.IO 线程,避免主线程卡顿。

优点

  • 确保 fetchData()scope 结束时自动取消,避免内存泄漏。
  • 更易管理协程生命周期

**示例:使用 **CoroutineScope

  • 管理协程的生命周期,确保协程不会泄漏(例如 Activity 销毁时自动取消协程)。
  • Android 中常用 viewModelScope(ViewModel 内)或 lifecycleScope(Activity/Fragment 内)。
class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            val data = fetchData()
            updateUI(data)
        }
    }
}

Dispatchers(调度器)

协程可以指定运行在哪个线程:

Kotlin 提供了 3 种主要的 Dispatchers(调度器):

Dispatcher作用
Dispatchers.Main主线程(UI 更新,Android 开发常用)
Dispatchers.IOIO 线程(适用于数据库、网络请求)
Dispatchers.DefaultCPU 密集型(适用于计算任务)
Dispatchers.Unconfined不指定线程,继承调用方

示例:切换调度器

fun main() = runBlocking {
    launch(Dispatchers.IO) {
        val data = fetchData()
        withContext(Dispatchers.Main) {
            println(data)  // 切换回主线程更新 UI
        }
    }
}
  • withContext(Dispatchers.Main) {}切换线程,在 UI 线程更新数据。

协程取消

协程可以手动取消:

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("执行任务 $i ...")
            delay(500L)
        }
    }
    delay(2000L)  // 运行 2 秒后取消
    job.cancel()
    println("协程已取消")
}

🔹 输出(只运行 4 次后取消):

执行任务 0 ...
执行任务 1 ...
执行任务 2 ...
执行任务 3 ...
协程已取消

协程的取消是协作式的(Cooperative),即:

  • 只有可挂起的函数(如 delay())才能响应取消。
  • 如果协程一直在计算任务,不会自动取消,需主动检测 isActive
fun main() = runBlocking {
    val job = launch {
        var i = 0
        while (isActive) {  // 监听协程是否活跃
            println("执行任务 ${i++} ...")
        }
    }
    delay(2000L)
    job.cancel()  // 取消协程
}

结构化并发(CoroutineScope)

如果你在 launch 里启动多个子协程,当父协程取消时,子协程也会被自动取消

fun main() = runBlocking {
    val parentJob = launch {
        launch {
            repeat(10) {
                println("子协程执行中 $it")
                delay(500L)
            }
        }
    }
    delay(2000L)
    parentJob.cancel()  // 取消父协程,子协程也会终止
    println("所有协程已取消")
}

总结

Kotlin 协程 提供了一种简单高效的方式来实现异步编程,它的优势:

  1. 轻量级,不会像线程那样消耗大量资源。
  2. 不会阻塞主线程,UI 线程流畅不卡顿。
  3. 结构化并发,避免内存泄漏,任务管理更方便。
  4. 支持挂起(suspend)函数,让异步代码写起来像同步代码。

问题

是不是任意一个代码块其实都可以相当于一个作用域在其中用协程?

是的,几乎所有代码块都可以作为一个作用域,在其中启动协程,但 不同作用域的生命周期、特性和适用场景不同。


1. 什么是“作用域”?

在 Kotlin 协程中,作用域(Scope) 指的是 协程运行的上下文,它决定了:

  1. 协程的生命周期(何时启动、何时取消)
  2. 协程在哪个线程执行Dispatchers.MainDispatchers.IO 等)
  3. 协程的层级结构(父协程取消时,子协程是否被取消)
    Kotlin 提供了 多种作用域,可以在不同的代码块中使用协程。
2. Kotlin 常见的协程作用域

下面列出了 可以作为协程作用域的代码块,并比较它们的特点。

作用域使用方式特点适用场景
GlobalScope.launch {}全局作用域不推荐,生命周期长,可能导致内存泄漏长时间运行的任务(不推荐)
viewModelScope.launch {}ViewModel 作用域绑定 ViewModel,ViewModel 销毁时自动取消网络请求、数据库查询
lifecycleScope.launch {}Activity/Fragment 作用域绑定 UI 生命周期,界面销毁时自动取消UI 相关任务(如动画、计时器)
coroutineScope {}局部作用域继承父协程的作用域,内部所有子协程都必须完成,才会继续执行外部代码需要并行执行多个任务时
liveData {}LiveData 作用域绑定 LiveData,当没有观察者时自动取消直接返回 LiveData 结果
runBlocking {}阻塞作用域主线程调用时会阻塞整个线程,一般用于测试不推荐在 UI 线程中使用
3. 示例:不同作用域的协程用法

(1)全局作用域(GlobalScope,不推荐)

GlobalScope.launch {
    delay(1000)
    println("GlobalScope 协程运行完毕")
}

特点:

  • 不受任何组件的生命周期影响
  • 手动管理取消,否则可能导致内存泄漏❌ 不推荐在 Android 开发中使用

(2)ViewModel 作用域(推荐用于网络请求)

class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch(Dispatchers.IO) {  // 在 IO 线程运行
            val result = getDataFromNetwork()
            withContext(Dispatchers.Main) {
                dataLiveData.value = result  // 回到主线程更新 UI
            }
        }
    }
}

特点:

  • viewModelScope 绑定 ViewModel 生命周期
  • ViewModel 销毁时,协程自动取消
  • 适用于网络请求、数据库查询等任务

(3)Activity/Fragment 作用域(lifecycleScope)

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        lifecycleScope.launch {
            delay(1000)
            println("Activity 作用域中的协程")
        }
    }
}

特点:

  • lifecycleScope** 绑定 Activity/Fragment 生命周期**
  • Activity/Fragment 关闭时,协程自动取消
  • 适用于 UI 任务,如动画、计时器

(4)局部作用域 coroutineScope {}(适用于并行任务)

suspend fun fetchData() {
    coroutineScope {
        launch {
            delay(1000)
            println("子任务1完成")
        }
        launch {
            delay(2000)
            println("子任务2完成")
        }
    }
    println("所有子任务完成,继续执行")
}

特点:

  • 继承父协程作用域
  • 所有子协程完成后,才会执行 coroutineScope 之后的代码
  • 适用于需要并行执行多个任务的情况

(5)LiveData 作用域 liveData {}(适用于 ViewModel 返回 LiveData)

fun searchPlaces(query: String) = liveData(Dispatchers.IO) {
    val result = try {
        val placeResponse = networkRequest(query)
        Result.success(placeResponse)
    } catch (e: Exception) {
        Result.failure<List<Place>>(e)
    }
    emit(result)  // 发送数据
}

特点:

  • liveData 代码块本身是一个协程
  • LiveData 没有观察者时,协程会自动取消
  • 适用于 ViewModel 直接返回 LiveData 的场景

(6)阻塞作用域 runBlocking {}(仅用于测试)

fun main() {
    runBlocking {
        launch {
            delay(1000)
            println("runBlocking 内部的协程")
        }
        println("主线程阻塞中")
    }
}

特点:

  • 会阻塞当前线程,直到协程执行完毕
  • 适用于测试,但不推荐在 Android UI 线程中使用
4. 总结

Kotlin 任何代码块都可以作为协程作用域,但选择合适的作用域很重要:

  • UI 任务lifecycleScope
  • 网络/数据库viewModelScope
  • 局部并发任务coroutineScope
  • LiveData 绑定数据liveData {}

如果你的代码块涉及协程,尽量选择合适的作用域,避免内存泄漏或意外取消任务!🚀

coroutineScope 和之前提到的 suspendCoroutine 很容易混淆

coroutineScopesuspendCoroutine 是 Kotlin 协程中两个容易混淆的概念,但它们的作用和用途是完全不同的。

1.coroutineScope

coroutineScope是一个挂起函数,用于创建一个新的协程作用域。它的主要特点是:

  • 结构化并发:它会等待其内部的所有协程完成后再结束。
  • 异常传播:如果coroutineScope内部的某个协程失败,它会取消所有其他协程,并将异常传播到外层。
  • 作用域管理:它不会启动新的协程,而是创建一个新的作用域,用于管理内部的协程。
使用场景:
  • 当你需要在挂起函数中启动多个协程,并确保它们都完成后再继续执行时。
  • 当你需要结构化并发,避免协程泄漏(即协程未正确取消)时。
示例:
suspend fun fetchData() = coroutineScope {
    val deferred1 = async { fetchFromNetwork1() }
    val deferred2 = async { fetchFromNetwork2() }
 
    val result1 = deferred1.await()
    val result2 = deferred2.await()
 
    result1 + result2
}
  • coroutineScope会等待deferred1deferred2都完成后再返回结果。
  • 如果deferred1deferred2失败,coroutineScope会取消另一个协程,并抛出异常。

2.suspendCoroutine

suspendCoroutine是一个底层挂起函数,用于将回调风格的代码转换为挂起函数。它的主要特点是:

  • 挂起协程:它会挂起当前协程,直到手动恢复。
  • 回调转换:它通常用于将基于回调的 API(如 Retrofit 的Call)转换为挂起函数。

使用场景:

  • 当你需要将回调风格的代码(如网络请求、数据库查询)转换为挂起函数时。
  • 当你需要手动控制协程的挂起和恢复时。

示例:

ksuspend fun fetchFromNetwork(): String = suspendCoroutine { continuation ->
    someCallbackBasedApi.enqueue(object : Callback<String> {
        override fun onResponse(call: Call<String>, response: Response<String>) {
            val body = response.body()
            if (body != null) {
                continuation.resume(body) // 恢复协程并返回数据
            } else {
                continuation.resumeWithException(RuntimeException("Response body is null"))
            }
        }
 
        override fun onFailure(call: Call<String>, t: Throwable) {
            continuation.resumeWithException(t) // 恢复协程并抛出异常
        }
    })
}
  • suspendCoroutine会挂起当前协程,直到调用continuation.resumecontinuation.resumeWithException
  • 它通常用于将回调风格的 API 转换为挂起函数。

3.coroutineScopevssuspendCoroutine
特性coroutineScopesuspendCoroutine
用途创建新的协程作用域,管理结构化并发将回调风格的代码转换为挂起函数
是否挂起协程是(等待内部协程完成)是(直到手动恢复)
是否启动新协程否(仅创建作用域)否(仅挂起当前协程)
异常传播是(内部协程失败会取消其他协程)是(通过resumeWithException抛出异常)
典型使用场景并发执行多个协程,并等待它们完成将回调 API 转换为挂起函数

4.代码对比

使用coroutineScope

suspend fun fetchData() = coroutineScope {
    val deferred1 = async { fetchFromNetwork1() }
    val deferred2 = async { fetchFromNetwork2() }
 
    deferred1.await() + deferred2.await()
}

使用suspendCoroutine

suspend fun fetchFromNetwork(): String = suspendCoroutine { continuation ->
    someCallbackBasedApi.enqueue(object : Callback<String> {
        override fun onResponse(call: Call<String>, response: Response<String>) {
            val body = response.body()
            if (body != null) {
                continuation.resume(body)
            } else {
                continuation.resumeWithException(RuntimeException("Response body is null"))
            }
        }
 
        override fun onFailure(call: Call<String>, t: Throwable) {
            continuation.resumeWithException(t)
        }
    })
}

5.总结
  • coroutineScope:用于创建新的协程作用域,管理结构化并发,适合在挂起函数中启动多个协程。
  • suspendCoroutine:用于将回调风格的代码转换为挂起函数,适合与基于回调的 API 交互。

更新: 2026-02-27 19:44:17
原文: https://www.yuque.com/dongpozhouzi-mshe3/zhm85g/ufwor7spuuflk22r


相关笔记