用了kt已经3年多,协程也一直在用,但是并没有好好的整体盘一遍。现在整个完全学习版,总算是又通畅了不少,加深了理解。
计划分成3篇完成讲解。

上篇,kotlin协程2025 通俗易懂三部曲之上篇 用法全解
中篇,讲解其他重要函数用法和并发。本文。
下篇,kotlin协程2025 通俗易懂三部曲之下篇 异常处理

1. runBlocking

它是顶级函数,它可以在任意地方使用。并不一定需要放在scope中。
它的作用就是为了卡住当前的线程来执行内部的协程任务块,执行完毕才继续往下走

runBlocking会阻塞当前线程(主线程则会让界面卡住)。这与别的Scope(GlobalScope, MainScope, ViewModelScope…)都不同,它们不会阻塞线程。

runBlocking 示例:

logt { "Run0......" }
runBlocking {
    logt { "Run1......" }
    delay(3000)
    launch {
        logt { "run3" }
        delay(1000)
        logt{"run4"}
    }
    launch(Dispatchers.Default, start = CoroutineStart.DEFAULT) {
        logt { "run5" }
        delay(1000)
        logt{"run6"}
    }
}
logt { "Run7 end......" }


MainThread: Run0......
MainThread: Run1......
MainThread: run5
Thread[61]: run3
MainThread: run4
Thread[61]: run6
MainThread: Run7 end......

#建议
一般情况,我们不使用它。比如SharedPref,DataStore的缓存读取可以考虑使用runBlocking直接调用,一般没什么问题,当做一个同步代码块使用。

2. withContext

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

这个挂起函数很有用:

  • 自动恢复原线程并返回结果
    在指定调度器(如 Dispatchers.IO)执行代码块后,会自动切换回调用前的线程(如主线程),避免手动切换的复杂性。
    代码块的最后一行作为返回值,直接传递给调用方。
suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    // 在IO线程执行网络请求
    "Result"
} // 自动切回调用线程(如Dispatchers.Main)

viewModelScope.launch {
   val data = fetchData()
   //这里是主线程,可以直接触发更新UI操作
   liveData.setValue(...)
}
  • 替代 async-await 的串行执行
    相比 async 的并行任务,withContext 以串行方式执行代码,无需处理 await() 的阻塞问题。
  • 作用域继承
    继承父协程的作用域(如 JobCoroutineExceptionHandler),确保异常和取消操作的传递性。
  • 可取消性
    内部代码块遵循协程的取消机制,响应父协程的取消请求。

3. suspendCancellableCoroutine / suspendCoroutine

public suspend inline fun <T> suspendCancellableCoroutine(
    crossinline block: (CancellableContinuation<T>) -> Unit
): T

将回调式异步操作转换为挂起函数‌ 的核心工具,相比 suspendCoroutine 额外支持 ‌协程取消响应‌。

suspendCoroutine略弱不做介绍,请使用suspendCancellableCoroutine为好。
从函数签名就可以看出,他与withContext的巨大差异。
withContext的block是一个有返回值的挂起函数。因此block最后一行写上返回结果,并且整个函数也得到这个结果,并且我之前也建议把withContext block的执行,用try-catch包裹返回State。
而suspendCancellableCoroutine的block并没有返回值。而且是一个inline函数。

  • 回调转挂起
    将传统回调 API(如网络请求、文件 IO)封装为挂起函数,实现同步式编程风格。

  • 取消支持
    通过 continuation.invokeOnCancellation 监听协程取消事件,及时释放资源(如关闭网络连接)。

  • 用法参考

    三个要素:invokeOnCancellation, resumeresumeWithException

    suspend fun request(url: String): Response = suspendCancellableCoroutine { continuation ->
        val call = OkHttpClient().newCall(Request.Builder().url(url).build())
        continuation.invokeOnCancellation { call.cancel() } //及时释放资源
        call.enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                continuation.resume(response)
            }
            override fun onFailure(call: Call, e: IOException) {
                continuation.resumeWithException(e)
            }
        })
    }
    

4. WithTimeoutOrNull/WithTimeout

public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend CoroutineScope.() -> T): T?
public suspend fun <T> withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T

超时后会抛出TimeoutCancellationException并自动触发invokeOnCancellation;
自动资源清理:确保网络请求等资源不会泄漏;
仍保持结构化并发特性,异常会向上传播;

示例:

suspend fun requestWithTimeout(
    url: String,
    timeoutMillis: Long = 5000L
): Response? = withTimeoutOrNull(timeoutMillis) {
    suspendCancellableCoroutine { continuation ->
        val call = OkHttpClient().newCall(Request.Builder().url(url).build())
        continuation.invokeOnCancellation { call.cancel() }
        
        call.enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                continuation.resume(response)
            }
            override fun onFailure(call: Call, e: IOException) {
                continuation.resumeWithException(e)
            }
        })
    }
}

5. coroutineScope & supervisorScope

他们都是挂起函数。只能在协程块中运行。

  • coroutineScope

    创建一个协程作用域并且在里面执行协程代码块。这个Scope继承外面scope的coroutineContext,但是重写它的Job。
    这个函数设计是为了并行分解工作。当任一协程失败,整个scope就失败,剩下的协程就会被cancel。当所有的子协程完成后才会返回。

  • supervisorScope
    ​ 创建一个协程作用域并且在里面执行协程代码块。这个Scope继承外面scope的coroutineContext,但是重写它的Job为SupervisorJob。当所有的子协程完成后才会返回。与coroutineScope不同, 一个子协程失败,并不会让整个scope去失败而且不会影响其他子协程,所以你要自己处理错误
    常用的viewModelScope,MainScope,lifecycleScope默认都是这种行为。

理解 coroutineScopesupervisorScope 之间的使用场景:
当你希望所有子协程因一个任务的失败而一同失败时,使用 coroutineScope
当你想独立处理子协程的失败并保持其他操作运行时,使用 supervisorScope

subScope.launch {
    coroutineScope { // 父协程
        launch { // 子协程A
            throw Exception("协同作用域异常") // ①异常触发
        }
        launch { // 子协程B
            delay(1000)
            println("永远不会执行") // ②被级联取消
        }
    }
}

supervisorScope { // 父协程
    launch { // 子协程A
        throw Exception("主从作用域异常") // ①异常被隔离
    }
    launch { // 子协程B
        delay(1000)
        println("正常执行") // ②不受影响
    }
}// ③异常不会传播给父协程

这里的作用,其实跟上篇链接 6. SupervisorJob() vs Job() 是差不多的。Job()属于给Scope定义约束。而本章节2个函数,是在比如ViewModel中使用的时候,不方便或者没必要创建自定义Scope,那么就可以通过这2个函数来帮助我们进行并发。

异常处理在【下篇】。

6. Scope的最佳实践

android已经提供了各种场景的Scope,而且自定义也只是调用cancel()而已。

    1. 全局使用,使用GlobalScope(官方不推荐)或者如第4条
    1. activity|fragment类中使用lifecycleScope
    1. viewModel类中使用viewModelScope
    1. 某个类(比如单例类)定义全局通用Scope:
    val scope = MainScope()
    val backgroundScope by lazy { CoroutineScope(Dispatchers.IO) }
    val backgroundScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
    //在自定义关闭的时候,调用
    scope.cancel()
    

注意:避免到处传递scope,把Scope当做参数传递是不好的编程习惯。对于业务类,我们应该只提供suspend函数。scope交给调用者自行决策。

源码scope自动cancel原理

一般情况我们不需要自定义Scope。
查看MainScope的源码:

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

查看lifecycleScope的源码:

//重定义
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

//继续追踪,是创建了LifecycleCoroutineScopeImpl
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        ...
        val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate //运行在主线程中。
            )
        ...
    }

//父类为这样定义的。基本上啥也没有,就是CoroutineScope的子类,放了一个lifecycle我们传入的
public actual abstract class LifecycleCoroutineScope internal actual constructor() :
    CoroutineScope {
    internal actual abstract val lifecycle: Lifecycle
    ...
}

internal class LifecycleCoroutineScopeImpl(
    override val lifecycle: Lifecycle,
    override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
    init {
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            coroutineContext.cancel()
        }
    }

    fun register() {
        launch(Dispatchers.Main.immediate) {
            if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
                lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
            } else {
                coroutineContext.cancel() //取消
            }
        }
    }

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
            lifecycle.removeObserver(this)
            coroutineContext.cancel() //取消
        }
    }
}

可以看出,其实就是就是实现了CoroutineScope的子类。通过监听lifecycle的过程来自动cancel,仅此而已。
我们可以类推到viewModelScope,基于想要监听的生命周期而自动cancel。

7. 并发处理

根据是依赖顺序还是并发分为3种:

7.1 顺序执行

通过withContext或者async+await等待前一个任务完成,接着进行第二个任务;

//withContext
lifecycleScope.launch {
     //step1
     val data = withContext(Dispatchers.IO) { //切换线程
         ...
         """ {"data":"request successfully."} """
     }
  	 ....
  	 //do step2...
}

//async await
lifecycleScope.launch {
    val deferred = async(Dispatchers.IO) {
        ...
        """ {"data":"request successfully."} """
    }
    val data = deferred.await()
    //do step2...
}
7.2 并发后统一等待
lifecycleScope.launch {
    ALogJ.t("start...")
    val deferred1 = async {
        delay(1000)
        "1111"
    }
    val deferred2 = async {
        delay(800)
        "2222"
    }
    val data1 = deferred1.await()
    val data2 = deferred2.await()
    //等待他们完成
    ALogJ.t("data1 $data1, data2 $data2")
}

//执行结果:
6.057 MainThread: start...
17.068 MainThread: data1 1111, data2 2222

可以看到async是立刻执行的,时间只有1秒。同时,所有Deferred.await以后,可以统一等待执行并发完成。这也能看出withContext和async、await的区别。

7.3 纯并发
  • 可以使用上面7.2的async 不用await即可;
  • 可以使用上面7.2的async 修改成launch即可;
  • 推荐使用5. coroutineScope & supervisorScope章节2个函数来完成:
    a. 使用coroutineScope来让任意异常取消整个执行;
    b. supervisorScope来让异常只隔离在单个子线程中。但是我仍然建议异常处理自己try-catch。

参考:

subScope.launch {
    coroutineScope { // 父协程
        launch { // 子协程A
            throw Exception("协同作用域异常") // ①异常触发
        }
        launch { // 子协程B
            delay(1000)
            println("永远不会执行") // ②被级联取消
        }
    }
}

supervisorScope { // 父协程
    launch { // 子协程A
        throw Exception("主从作用域异常") // ①异常被隔离
    }
    launch { // 子协程B
        delay(1000)
        println("正常执行") // ②不受影响
    }
}// ③异常不会传播给父协程

总结

这篇文章是Kotlin协程三部曲的中篇,主要讲解协程的重要函数用法和并发处理。文章介绍了协程函数:
runBlocking, withContext, suspendCancellableCoroutine, withTimeoutOrNull/withTimeout, coroutineScope和supervisorScope等。并介绍了如何根据场景使用哪种并发处理。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐