Android 架构指南:suspend 如何响应取消?别再给 Repository 加 `cancel()`

文章来源声明: 原文作者:潜龙勿用之化骨龙; 来源站点:掘金; 原文链接:https://juejin.cn/post/7690463587244687411; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

把生命周期交还给协程结构,用 awaitCancellation() 取代手动 cancel(),可减少样板与生命周期脱节风险。适合重资源初始化、连接持有等场景,非 Repository 通用模板。

### 前言

之前写过一篇: Android 架构指南之 Data 层不要再暴露 start/stop 了:用 Flow 接管生命周期

那篇文章讨论的是一个比较常见的问题:

repository<span>.start</span>()
repository<span>.stop</span>()

为什么一个 Data 层对象的生命周期,需要由调用方显式管理?

如果 Repository 提供的是持续变化的数据,Flow 本身就可以承担这件事情:

repository.observeUser()
<span>    .collect { ... }
</span>

调用方的 coroutine 被取消,collect 被取消,Flow 上游自然也就结束了。

这其实已经解决了一大类生命周期问题。

但还有一种情况,Flow 并不是最合适的表达方式。

比如:

<span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span>

这个操作可能不是简单地创建几个对象。

比如:

  • 初始化比较重的资源
  • 创建并持有一些需要主动释放的对象
  • 初始化底层组件或连接
  • 完成一些耗时的初始化工作

这些资源初始化完成之后,页面可能还会继续使用。

但当页面关闭、ViewModel 被销毁时,这些资源通常也需要及时释放。

于是生命周期变成了:

<span>init</span>
 ↓
初始化资源
 ↓
页面继续使用
 ↓
页面关闭
 ↓
coroutine 被取消
 ↓
释放资源

问题来了:

如果 init() 本身是一个 suspend 操作,它应该怎么感知到调用方的生命周期结束,并在取消时释放这些资源?

很多时候,我们第一反应还是:

<span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span>

<span><span>fun</span> <span>cancel</span><span>()</span></span>

然后:

viewModelScope<span>.launch</span> {
    repository<span>.init</span>()
}

<span>// 页面关闭</span>
repository<span>.cancel</span>()

但如果 init() 本身就在:

viewModelScope.launch {
    repository.<span>init</span>()
}

里面运行,那么这个 cancel() 真的还需要存在吗?

其实不需要。

不需要手动取消,这就是 Structured Concurrency 的优势。

我们来看,一个负责初始化重资源的 suspend 操作,如何响应调用方的 cancellation,并在页面生命周期结束时完成资源释放。


一、先看一个最简单的例子

很多 Android 项目的 Repository,会设计成这样:

<span>interface</span> <span>UserRepository</span> {
    <span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span>
    <span><span>fun</span> <span>cancel</span><span>()</span></span>
}

调用方:

viewModelScope<span>.launch</span> {
    repository<span>.init</span>()
}

<span>// 页面销毁</span>
repository<span>.cancel</span>()

看起来没什么问题。

但仔细想想:

为什么调用方需要知道 Repository 什么时候该取消?

如果 Repository 本身运行在 viewModelScope 中,那么 ViewModel 销毁的时候,viewModelScope 本来就会自动取消。

既然如此,Repository 为什么还需要额外提供一个 cancel()?

这里其实已经暴露出了一个问题:

我们正在用一个额外的 API,手动管理本来就属于 coroutine 的生命周期。

这篇文章从一个很小的例子开始,看看能不能把这件事情做得更自然。


二、先把问题说清楚:初始化完成,不代表资源生命周期结束

假设我们有一个 Repository:

<span>class</span> <span>MyRepository</span> {

    <span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span> {
        setup()
    }

    <span>private</span> <span><span>fun</span> <span>setup</span><span>()</span></span> {
        println(<span>"setup"</span>)
    }
}

ViewModel:

<span>class</span> <span>MyViewModel</span> : <span>ViewModel</span>() {

    <span><span>fun</span> <span>initRepository</span><span>()</span></span> {
        viewModelScope.launch {
            repository.<span>init</span>()
        }
    }
}

这里没有任何问题。

init() 执行:

ViewModel
    ↓
viewModelScope
    ↓
launch
    ↓
repository<span>.init</span>()
    ↓
<span>setup</span>()
    ↓
<span>init</span>() 完成

但是这里马上出现一个问题。

如果 init() 很快就执行完了:

viewModelScope.launch {
    repository.<span>init</span>()
}

这个 coroutine 里的任务也就结束了。

之后页面关闭:

ViewModel 被销毁
<span>      ↓
viewModelScope.cancel()
</span>

这时候已经没有什么 Repository 的 coroutine 可以取消了。

这本身并不是问题。

因为一个已经完成的操作,本来就不需要再响应取消。

真正的问题是另一种情况:

初始化阶段结束了,但初始化出来的资源生命周期还没有结束。

例如:

<span>init</span>
 ↓
初始化重资源
 ↓
资源初始化完成
 ↓
页面继续使用
 ↓
页面关闭
 ↓
释放资源

这时候:

<span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span>

如果执行完马上返回,那么 Repository 就失去了一个非常重要的东西:

资源生命周期。

我们真正需要解决的不是:

“初始化怎么做?”

而是:

“初始化完成之后,如何让这个资源的生命周期继续跟随调用方?”


三、能不能让 init() 一直等到取消?

可以。

Kotlin 协程提供了一个非常适合这个场景的函数:

<span>awaitCancellation</span>()

例如:

<span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span> {
    setup()
    awaitCancellation()
}

它的意思不是:

“阻塞线程,等着取消。”

而是:

“挂起当前协程,一直等到它被取消。”

这是一个非常重要的区别。

这里的 awaitCancellation() 可以理解成:

初始化完成
     ↓
资源已经准备好
     ↓
但是资源生命周期还没有结束
     ↓
当前 <span>coroutine</span> 继续保持
     ↓
直到调用方取消

所以:

<span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span> {
    setup()
    awaitCancellation()
}

表达的并不是:

初始化永远不会结束。

而是:

初始化已经结束,但这个生命周期型操作还没有结束。


四、awaitCancellation() 会不会阻塞线程?

不会。

例如:

<span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span> {
    setup()
    awaitCancellation()
}

执行到:

<span>awaitCancellation</span>()

之后:

Coroutine
    │
    ├── <span>setup</span>()
    │
    ↓
<span>awaitCancellation</span>()
    │
    │
    │ 协程挂起
    │
    ↓
等待 Cancellation

它不会一直占用当前线程。

这和:

Thread.<span>sleep</span>(...)

完全不是一回事。

Thread.sleep():

线程
 ↓
被占用
 ↓
等待
 ↓
继续执行

而 awaitCancellation():

协程
 ↓
挂起
 ↓
线程可以去干其他事情
 ↓
收到取消
 ↓
进入取消流程

所以它不会把主线程卡住。

这一点非常重要。

因为我们想要的是:

保持 coroutine 的生命周期,而不是占住一个线程。


五、现在问题就变得有意思了

我们把 Repository 改成:

<span>class</span> <span>MyRepository</span> {

    <span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span> {
        setup()
        awaitCancellation()
    }

    <span>private</span> <span><span>fun</span> <span>setup</span><span>()</span></span> {
        println(<span>"setup"</span>)
    }
}

ViewModel:

<span><span>fun</span> <span>initRepository</span><span>()</span></span> {
    viewModelScope.launch {
        repository.<span>init</span>()
    }
}

现在整个生命周期就变成了:

ViewModel
    │
    ↓
viewModelScope
    │
    ↓
launch
    │
    ↓
repository<span>.init</span>()
    │
    ├── <span>setup</span>()
    │
    └── <span>awaitCancellation</span>()
             │
             │
             │ 等待
             │
             ↓
        页面继续使用
             │
             ↓
        ViewModel 销毁
             │
             ↓
        viewModelScope<span>.cancel</span>()
             │
             ↓
        <span>init</span>() 被取消

这里其实发生了一件非常重要的事情:

我们没有手动取消 Repository。

没有:

repository<span>.cancel</span>()

也没有:

repository<span>.stop</span>()

甚至 Repository 根本不需要知道:

“页面什么时候关闭?”

因为:

repository.<span>init</span>()

本身就是 viewModelScope 中 coroutine 的一部分。

当父 coroutine 被取消时,子 coroutine 会自然地跟着取消。

这就是 Structured Concurrency 的优势。

可以把它理解成:

父 <span>coroutine</span>
    │
    ├── 子 <span>coroutine</span> A
    │
    ├── 子 <span>coroutine</span> B
    │
    └── repository.init()

父协程取消:

Parent cancel
<span>     ↓
Child cancel
     ↓
cleanup
</span>

所以:

取消不是 Repository 额外提供的一种能力,而是 coroutine 生命周期自然产生的结果。

这也是 Structured Concurrency 和传统手动生命周期管理最大的区别之一。

以前我们可能习惯:

<span>init</span>()

<span>// 页面关闭</span>
<span>cancel</span>()

需要自己记住什么时候 cancel()。

现在则变成:

viewModelScope.launch {
    repository.<span>init</span>()
}

谁创建 coroutine,谁就拥有这个 coroutine 的生命周期。

页面销毁:

ViewModel
    ↓
viewModelScope<span>.cancel</span>()
    ↓
repository<span>.init</span>() 自动收到取消

不需要手动取消。

当然,这里还有一个问题:

init() 为什么没有在 setup() 完成之后立即返回?

因为我们的场景并不是:

初始化
 ↓
完成
 ↓
结束

而是:

初始化资源
 ↓
资源初始化完成
 ↓
页面继续使用
 ↓
页面关闭
 ↓
资源释放

所以这里才需要:

<span>awaitCancellation</span>()

它负责的不是“取消”。

取消由 Structured Concurrency 负责传播。

awaitCancellation() 只是让这个生命周期型的 suspend 操作继续存在,直到父 coroutine 被取消。

这两个概念一定要分清:

Structured Concurrency
        ↓
负责生命周期和取消传播

<span>awaitCancellation</span>()
        ↓
让 <span>init</span>() 保持到取消发生

两者结合起来,才形成完整的资源生命周期管理。


六、取消是怎么进入 Repository 的?

现在我们已经知道:

viewModelScope.launch {
    repository.<span>init</span>()
}

这里不需要:

repository<span>.cancel</span>()

因为:

viewModelScope
    ↓
repository.<span>init</span>()

本身就建立了父子关系。

当 ViewModel 被销毁:

viewModelScope<span>.cancel</span>()
        ↓
child coroutine 被取消
        ↓
repository<span>.init</span>()
        ↓
CancellationException

那么 Repository 怎么处理这个取消?

很简单:

<span>class</span> <span>MyRepository</span> {

    <span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span> {
        <span>try</span> {
            setup()
            awaitCancellation()
        } <span>catch</span> (e: CancellationException) {
            cleanup()
            <span>throw</span> e
        }
    }

    <span>private</span> <span><span>fun</span> <span>setup</span><span>()</span></span> {
        println(<span>"setup"</span>)
    }

    <span>private</span> <span><span>fun</span> <span>cleanup</span><span>()</span></span> {
        println(<span>"cleanup"</span>)
    }
}

整个过程:

viewModelScope<span>.cancel</span>()
        ↓
<span>init</span>() 收到 cancellation
        ↓
CancellationException
        ↓
<span>cleanup</span>()
        ↓
throw e
        ↓
取消继续向上传播

这里最重要的一点就是:

Repository 不需要主动发起取消。

它只需要:

响应调用方已经发生的取消。

这就是 Structured Concurrency 带来的一个非常大的好处:

调用方负责拥有生命周期,子任务负责响应生命周期。

因此,我们不需要再设计一套额外的:

<span>start</span>()
<span>stop</span>()

<span>init</span>()
<span>cancel</span>()

来同步两个生命周期。

生命周期已经通过 coroutine 的父子关系连接起来了。


七、为什么一定要 throw e?

这里有一个很容易踩的坑。

不要这样:

catch (e: CancellationException) {
    <span>cleanup</span>()
}

因为这样实际上把 CancellationException 吃掉了。

正确做法:

catch (e: CancellationException) {
    <span>cleanup</span>()
    throw e
}

原因很简单:

取消不是普通业务异常。

它是协程生命周期的一部分。

取消发生之后,Repository 可以做清理,但不应该阻止取消继续向上传播。

所以可以把它理解成:

CancellationException
<span>        │
        ├── Repository 做 cleanup
        │
        └── 继续向上抛
</span>

而不是:

CancellationException
<span>        │
        ↓
Repository 吞掉
        │
        ↓
取消状态被隐藏
</span>

所以:

catch (e: CancellationException) {
    <span>cleanup</span>()
    throw e
}

这三个动作其实分别对应三个职责:

收到取消
   ↓
释放资源
   ↓
继续传播取消


八、还有一个细节:cleanup 如果需要挂起怎么办?

假设:

<span>suspend</span> <span><span>fun</span> <span>cleanup</span><span>()</span></span> {
    <span>// some suspend work</span>
}

这时候:

catch (e: CancellationException) {
    <span>cleanup</span>()
    throw e
}

可能存在问题。

因为当前 coroutine 已经处于取消状态。

如果 cleanup() 内部还有需要挂起的操作,那么它也可能立即响应取消。

例如:

catch (e: CancellationException) {
    <span>cleanup</span>()
    throw e
}

如果 cleanup() 内部:

<span>suspend</span> <span><span>fun</span> <span>cleanup</span><span>()</span></span> {
    someSuspendFunction()
}

这个挂起操作也可能因为当前 coroutine 已经取消而无法正常完成。

如果这个 cleanup 必须保证执行完成,可以使用:

catch (e: CancellationException) {
    <span>withContext</span>(NonCancellable) {
        <span>cleanup</span>()
    }

    throw e
}

意思就是:

当前协程已经被取消,但这段必要的清理工作必须执行完。

然后再:

<span>throw</span> e

继续把取消传播出去。

不过不要滥用 NonCancellable。

它适合的是:

  • 必须释放的资源
  • 必须执行的 unregister
  • 必须完成的状态恢复
  • 必须保证一致性的清理操作

而不是拿来让一个已经取消的任务继续干一大堆工作。

NonCancellable 应该是一个非常小的保护区域。


九、为什么这一切都成立?Structured Concurrency

看到这里,其实已经可以看出:

真正重要的并不是:

<span>awaitCancellation</span>()

而是它背后的生命周期关系。

我们来看一个错误的写法:

<span>class</span> <span>MyRepository</span> {

    <span><span>fun</span> <span>init</span><span>()</span></span> {
        CoroutineScope(Dispatchers.IO).launch {
            doSomething()
        }
    }
}

然后:

viewModelScope.launch {
    repository.<span>init</span>()
}

表面上看起来生命周期绑定了。

实际上没有。

因为:

viewModelScope
    │
    └── repository<span>.init</span>()
             │
             └── <span>CoroutineScope</span>(IO)<span>.launch</span>
                        │
                        └── <span>doSomething</span>()

Repository 又创建了一个新的 CoroutineScope。

这个新的 Job 并不是 viewModelScope 的 child。

所以:

ViewModel 销毁
      ↓
viewModelScope.cancel()
      ↓
ViewModel 相关 <span>coroutine</span> 被取消
      ↓
Repository 自己创建的 <span>coroutine</span>
      ↓
还在运行

这就是生命周期脱节。

真正理想的结构是:

ViewModel
    │
    ↓
viewModelScope
    │
    ↓
launch
    │
    ↓
Repository<span>.init</span>()
    │
    ├── <span>setup</span>()
    │
    └── <span>awaitCancellation</span>()

也就是说:

Repository 使用调用方提供的 coroutine 上下文,而不是自己偷偷创建一个新的生命周期。

这就是 Structured Concurrency 的核心思想之一:

父协程负责子协程的生命周期。

父取消:

Parent cancel
<span>     ↓
Child cancel
     ↓
Child cleanup
</span>

所以 Repository 不需要自己维护:

private val <span>scope</span> = CoroutineScope(...)

也不需要维护:

<span>private</span> <span>var</span> job: Job? = <span>null</span>

更不需要额外暴露:

<span><span>fun</span> <span>cancel</span><span>()</span></span>

如果这个操作本来就属于调用方的生命周期,那么让 coroutine 结构表达这种关系,通常更加自然。


十、这也解释了为什么 Repo 不需要 cancel()

如果 Repository API 是:

<span>interface</span> <span>Repository</span> {

    <span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span>
}

调用方:

viewModelScope.launch {
    repository.<span>init</span>()
}

那么:

ViewModel
    │
    └── viewModelScope
          │
          └── repository.<span>init</span>()

Repository 根本不需要知道:

“你什么时候页面关闭?”

它只需要遵守协程的取消规则。

页面生命周期:

ViewModel destroyed
<span>        ↓
viewModelScope.cancel()
</span>

自然就会传递到:

repository.<span>init</span>()

这就是一个非常漂亮的职责分离。

调用方决定:

这个操作属于谁的生命周期?

Repository 决定:

资源初始化、使用以及释放应该怎么做?

两者通过 coroutine 的 cancellation 自然连接起来。


十一、调用方只负责生命周期

调用方:

viewModelScope.launch {
    repository.<span>init</span>()
}

负责的是:

这个操作属于谁的生命周期?

Repository:

<span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span> {
    ...
}

负责的是:

我要怎么初始化和管理这些资源?

而不是:

repository<span>.init</span>()
repository<span>.cancel</span>()

让调用方同时管理:

  • 启动
  • 生命周期
  • 取消
  • 清理

这样职责就混在一起了。

更自然的结构是:

调用方
<span>    ↓
决定生命周期
    ↓
coroutine
</span>
Repository
<span>    ↓
执行初始化
    ↓
持有资源
    ↓
响应 cancellation
    ↓
释放资源
</span>

所以:

调用方不需要管理 Repository 的取消,它只需要管理自己的 coroutine 生命周期。


十二、一个完整的例子

Repository:

<span>class</span> <span>MyRepository</span> {

    <span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span> {
        <span>try</span> {
            log(<span>"init start"</span>)
            setup()
            log(<span>"setup finished"</span>)

            awaitCancellation()

        } <span>catch</span> (e: CancellationException) {
            log(<span>"received cancellation"</span>)
            cleanup()
            <span>throw</span> e
        }
    }

    <span>private</span> <span><span>fun</span> <span>setup</span><span>()</span></span> {
        log(<span>"setup"</span>)
    }

    <span>private</span> <span><span>fun</span> <span>cleanup</span><span>()</span></span> {
        log(<span>"cleanup"</span>)
    }

    <span>private</span> <span><span>fun</span> <span>log</span><span>(message: <span>String</span>)</span></span> {
        println(
            <span>"[<span>${Thread.currentThread().name}</span>] <span>$message</span>"</span>
        )
    }
}

ViewModel:

<span>class</span> <span>MyViewModel</span> : <span>ViewModel</span>() {

    <span><span>fun</span> <span>initRepository</span><span>()</span></span> {
        viewModelScope.launch {
            repository.<span>init</span>()
        }
    }
}

运行过程:

<span>[main]</span> init start

<span>[main]</span> setup

<span>[main]</span> setup finished

                    ↓
              <span>awaitCancellation</span>()
                    ↓
                协程挂起
                    ↓
                页面继续使用
                    ↓
                页面关闭
                    ↓
            viewModelScope<span>.cancel</span>()
                    ↓

<span>[main]</span> received cancellation

<span>[main]</span> cleanup

这里最重要的是:

awaitCancellation() 并没有占用线程。

它只是让这个 coroutine 保持生命周期。

而真正让 init() 在页面关闭时结束的,是:

viewModelScope<span>.cancel</span>()

以及 Structured Concurrency 带来的取消传播:

viewModelScope
      ↓
child coroutine
      ↓
repository.<span>init</span>()
      ↓
CancellationException

所以:

Repository 没有主动取消自己,也没有被调用方手动取消。

它只是响应父 coroutine 的 cancellation。


十三、这个模式并不是让所有 Repo 都这么写

这里需要特别强调:

<span>awaitCancellation</span>()

不是一个“Repository 标准模板”。

如果你的 Repository 方法只是一次性的:

<span>suspend</span> <span><span>fun</span> <span>loadUser</span><span>()</span></span>: User

那么完全不需要:

<span>awaitCancellation</span>()

正常:

viewModelScope.launch {
    val <span>user</span> = repository.loadUser()
}

就够了。

因为:

<span>loadUser</span>()
    ↓
完成
    ↓
coroutine 结束

这就是正确的生命周期。

awaitCancellation() 适合的是:

初始化完成之后,资源本身还需要继续存在,并且应该随着调用方 coroutine 的生命周期结束而释放。

例如:

  • 初始化比较重的资源
  • 创建并持有需要主动释放的对象
  • 初始化底层组件
  • 建立需要主动关闭的连接
  • 持有某些必须在生命周期结束时释放的资源

这时候才有必要考虑:

<span>awaitCancellation</span>()

所以不要看到 suspend 就条件反射地加:

<span>awaitCancellation</span>()

它解决的是一个非常具体的问题:

资源初始化完成了,但资源生命周期还没有结束。


十四、如果本身就是持续的数据,Flow 依然更自然

这里也需要和上一篇文章联系起来。

如果 Repository 本身提供的是持续变化的数据:

<span><span>fun</span> <span>observeUser</span><span>()</span></span>: Flow<User>

那么通常不需要自己:

<span>awaitCancellation</span>()

调用方:

viewModelScope.launch {
    repository.observeUser().<span>collect</span> { <span>user</span> <span>-</span><span>></span>
        <span>/</span><span>/</span> <span>update</span> UI
    }
}

当 ViewModel 销毁:

viewModelScope<span>.cancel</span>()
        ↓
collect cancelled
        ↓
<span>Flow</span> 上游取消
        ↓
Repository 停止工作

这其实也是 Structured Concurrency。

所以:

Flow 更适合表达持续的数据流;而 awaitCancellation() 更适合表达一个资源初始化完成之后,仍然需要继续持有资源的生命周期型 suspend 操作。

两者解决的问题并不完全一样。

可以简单理解成:

持续的数据
    ↓
   <span>Flow</span>
    ↓
collect 生命周期


资源型操作
    ↓
 suspend
    ↓
<span>awaitCancellation</span>()
    ↓
coroutine 生命周期

所以上一篇文章讲:

Data 层不要再暴露 start/stop,用 Flow 接管生命周期。

这一篇则继续往下讨论:

如果这个场景不是 Flow,而是一个生命周期型 suspend 操作,它又应该如何响应取消?

答案还是同一个:

让 coroutine 自己管理生命周期,而不是额外设计一套 start/stop 或 init/cancel。


十五、回到那个最初的问题

为什么 Repository 要不要提供:

<span>cancel</span>()

真正的问题并不是:

“有没有必要提供一个 cancel 方法?”

而是:

这个 Repository 的 coroutine 到底属于谁?

如果它属于:

ViewModel
   ↓
viewModelScope

那么:

repository<span>.cancel</span>()

通常就是多余的。

因为调用方已经拥有生命周期。

正确的设计是:

viewModelScope.launch {
    repository.<span>init</span>()
}

Repository:

<span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span> {
    <span>try</span> {
        setup()
        awaitCancellation()
    } <span>catch</span> (e: CancellationException) {
        cleanup()
        <span>throw</span> e
    }
}

最终形成:

              ViewModel
                 │
                 ↓
           viewModelScope
                 │
                 ↓
               launch
                 │
                 ↓
             Repository
                 │
                 ↓
               <span>init</span>()
                 │
                 ↓
           初始化重资源
                 │
                 ↓
       <span>awaitCancellation</span>()
                 │
                 │
           页面继续使用
                 │
                 ↓
       页面关闭 / VM 清除
                 │
                 ↓
       viewModelScope<span>.cancel</span>()
                 │
                 ↓
        CancellationException
                 │
                 ↓
              <span>cleanup</span>()

没有:

repository<span>.cancel</span>()

没有:

GlobalScope

没有:

<span>CoroutineScope</span>(Dispatchers.IO)

也没有额外的生命周期同步代码。

生命周期已经存在于 coroutine 的父子关系里。


最后

上一篇文章讲的是:

不要让调用方通过 start/stop 管理 Data 层的生命周期,让 Flow 接管它。

这一次,我们继续往下走了一步。

当一个操作本身适合用 suspend 表达时,也不应该重新退回到:

<span>init</span>()
<span>cancel</span>()

而应该让 coroutine 自己携带生命周期。

调用方决定:

viewModelScope.launch {
    repository.<span>init</span>()
}

Repository 决定:

<span>suspend</span> <span><span>fun</span> <span>init</span><span>()</span></span> {
    <span>try</span> {
        setup()
        awaitCancellation()
    } <span>catch</span> (e: CancellationException) {
        cleanup()
        <span>throw</span> e
    }
}

于是:

谁创建 <span>coroutine</span>
        ↓
谁拥有生命周期
        ↓
父协程取消
        ↓
子协程自动取消
        ↓
Repository 响应 cancellation
        ↓
释放自己持有的资源

这里最值得记住的,其实不是:

<span>awaitCancellation</span>()

而是:

不需要手动取消,这就是 Structured Concurrency 的优势。

awaitCancellation() 只是技术手段。

它解决的是:

初始化完成了,但资源生命周期还没有结束。

而 Structured Concurrency 解决的是更大的问题:

这个资源到底属于谁的生命周期?

如果它属于 viewModelScope,那么就让它成为 viewModelScope 的一部分。

页面销毁:

ViewModel
    ↓
viewModelScope<span>.cancel</span>()
    ↓
child coroutine 自动取消
    ↓
Repository 响应 CancellationException
    ↓
<span>cleanup</span>()

整个过程没有:

repository<span>.cancel</span>()

没有:

repository<span>.stop</span>()

也不需要调用方额外记住:

“页面销毁的时候,我是不是还漏掉了一个资源释放?”

因为生命周期已经进入了 coroutine 的结构。

以前我们习惯:

<span>init</span>()
...
<span>cancel</span>()

现在则是:

创建 <span>coroutine</span>
    ↓
资源属于这个 <span>coroutine</span>
    ↓
父协程结束
    ↓
子协程自动取消
    ↓
资源清理

取消不是一个额外的业务 API,而是 coroutine 生命周期自然产生的结果。

这就是 Structured Concurrency 真正漂亮的地方。

好的架构,有时候不是增加一个 cancel()。

而是:

让这个 cancel() 根本不需要存在。