[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f27w1rov4qcdyr":3},{"_id":4,"slug":5,"title":6,"subtitle":7,"kind":8,"cards":9,"tags":34,"categories":35,"source":36,"lang":39,"author":40,"audioState":43,"stats":44,"publishedAt":47,"renderer":48},"6abaa979ca21c797c7e9a230","android-suspend-repository-cancel-0742d78e","Android 架构指南：suspend 如何响应取消？别再给 Repository 加 `cancel()`","为什么一个 Data 层对象的生命周期，需要由调用方显式管理？ 如果 Repository 提供的是持续变化的数据，Flow 本身就可以承担这件事情： 调用方的 coroutine 被取消，collect 被取消，Flow 上游自然也就结束了。 但还有一种情况，Flow 并不是最合适的表达方式。 但当页面关闭、ViewModel 被销毁时，这些资源通常也需要及时释放。 如果 init()…","news",[10,14,19,24,29],{"headline":6,"body":11,"imageUrl":12,"images":13},"为什么一个 Data 层对象的生命周期，需要由调用方显式管理？ 如果 Repository 提供的是持续变化的数据，Flow 本身就可以承担这件事情： 调用方的 coroutine 被取消，collect 被取消，Flow 上游自然也就结束了。 但还有一种情况，Flow 并不是最合适的表达方式。 但当页面关闭、ViewModel 被销毁时，这些资源通常也需要及时释放。 如果 init() 本身是一个 suspend 操作，它应该怎么感知到调用方的生命周期结束，并在取消时释放这些资源？ 里面运行，那么这个 cancel() 真的还需要存在吗？ 不需要手动取消，这就是 Structured Concurrency 的优势。 我们来看，一个负责初始化重资源的 suspend 操作，如何响应调用方的 cancellation，并在页面生命周期结束时完成资源释放。 很多 Android 项目的 Repository，会设计成这样： 为什么调用方需要知道 Repository 什么时候该取消？ 如果 Repository 本身运行在 viewModelScope 中，那么 ViewModel 销毁的时候，viewModelScope 本来就会自动取消。 既然如此，Repository 为什么还需要额外提供一个 cancel()？ 我们正在用一个额外的 API，手动管理本来就属于 coroutine 的生命周期。 我们正在用一个额外的 API，手动管理本来就属于 coroutine 的生命周期。 这篇文章从一个很小的例子开始，看看能不能把这件事情做得更自然。 二、先把问题说清楚：初始化完成，不代表资源生命周期结束 这时候已经没有什么 Repository 的 coroutine 可以取消了。 初始化阶段结束了，但初始化出来的资源生命周期还没有结束。 初始化阶段结束了，但初始化出来的资源生命周期还没有结束。 如果执行完马上返回，那么 Repository 就失去了一个非常重要的东西： “初始化完成之后，如何让这个资源的生命周期继续跟随调用方？”","\u002Fapi\u002Fmedia\u002Fposts\u002Fandroid-suspend-repository-cancel-0742d78e\u002F0.webp",{"local":12},{"headline":15,"body":16,"imageUrl":17,"images":18},"“初始化完成之后，如何让这个资源的生命周期继续跟随调用方？” Kotlin 协程提供了一个非常适合这个场景的函数： 这里的…","“初始化完成之后，如何让这个资源的生命周期继续跟随调用方？” Kotlin 协程提供了一个非常适合这个场景的函数： 这里的 awaitCancellation() 可以理解成： 四、awaitCancellation() 会不会阻塞线程？ 保持 coroutine 的生命周期，而不是占住一个线程。 保持 coroutine 的生命周期，而不是占住一个线程。 本身就是 viewModelScope 中 coroutine 的一部分。 当父 coroutine 被取消时，子 coroutine 会自然地跟着取消。 这就是 Structured Concurrency 的优势。 取消不是 Repository 额外提供的一种能力，而是 coroutine 生命周期自然产生的结果。 取消不是 Repository 额外提供的一种能力，而是 coroutine 生命周期自然产生的结果。 这也是 Structured Concurrency 和传统手动生命周期管理最大的区别之一。 谁创建 coroutine，谁就拥有这个 coroutine 的生命周期。 init() 为什么没有在 setup() 完成之后立即返回？ 取消由 Structured Concurrency 负责传播。 awaitCancellation() 只是让这个生命周期型的 suspend 操作继续存在，直到父 coroutine 被取消。 这就是 Structured Concurrency 带来的一个非常大的好处： 生命周期已经通过 coroutine 的父子关系连接起来了。 因为这样实际上把 CancellationException 吃掉了。 取消发生之后，Repository 可以做清理，但不应该阻止取消继续向上传播。 八、还有一个细节：cleanup 如果需要挂起怎么办？ 如果 cleanup() 内部还有需要挂起的操作，那么它也可能立即响应取消。 这个挂起操作也可能因为当前 coroutine 已经取消而无法正常完成。 如果这个 cleanup 必须保证执行完成，可以使用：","\u002Fapi\u002Fmedia\u002Fposts\u002Fandroid-suspend-repository-cancel-0742d78e\u002F1.webp",{"local":17},{"headline":20,"body":21,"imageUrl":22,"images":23},"当前协程已经被取消，但这段必要的清理工作必须执行完。 当前协程已经被取消，但这段必要的清理工作必须执行完。 NonCancellable…","当前协程已经被取消，但这段必要的清理工作必须执行完。 当前协程已经被取消，但这段必要的清理工作必须执行完。 NonCancellable 应该是一个非常小的保护区域。 九、为什么这一切都成立？Structured Concurrency Repository 又创建了一个新的 CoroutineScope。 这个新的 Job 并不是 viewModelScope 的 child。 Repository 使用调用方提供的 coroutine 上下文，而不是自己偷偷创建一个新的生命周期。 Repository 使用调用方提供的 coroutine 上下文，而不是自己偷偷创建一个新的生命周期。 这就是 Structured Concurrency 的核心思想之一： 如果这个操作本来就属于调用方的生命周期，那么让 coroutine 结构表达这种关系，通常更加自然。 十、这也解释了为什么 Repo 不需要 cancel() 两者通过 coroutine 的 cancellation 自然连接起来。 调用方不需要管理 Repository 的取消，它只需要管理自己的 coroutine 生命周期。 调用方不需要管理 Repository 的取消，它只需要管理自己的 coroutine 生命周期。 awaitCancellation() 并没有占用线程。 以及 Structured Concurrency 带来的取消传播： Repository 没有主动取消自己，也没有被调用方手动取消。 Repository 没有主动取消自己，也没有被调用方手动取消。 它只是响应父 coroutine 的 cancellation。 如果你的 Repository 方法只是一次性的： awaitCancellation() 适合的是： 初始化完成之后，资源本身还需要继续存在，并且应该随着调用方 coroutine 的生命周期结束而释放。 初始化完成之后，资源本身还需要继续存在，并且应该随着调用方 coroutine 的生命周期结束而释放。 十四、如果本身就是持续的数据，Flow 依然更自然 如果","\u002Fapi\u002Fmedia\u002Fposts\u002Fandroid-suspend-repository-cancel-0742d78e\u002F2.webp",{"local":22},{"headline":25,"body":26,"imageUrl":27,"images":28},"Repository 本身提供的是持续变化的数据： 这其实也是 Structured Concurrency。 Flow 更适合表达持续的数据流；而…","Repository 本身提供的是持续变化的数据： 这其实也是 Structured Concurrency。 Flow 更适合表达持续的数据流；而 awaitCancellation() 更适合表达一个资源初始化完成之后，仍然需要继续持有资源的生命周期型 suspend 操作。 Flow 更适合表达持续的数据流；而 awaitCancellation() 更适合表达一个资源初始化完成之后，仍然需要继续持有资源的生命周期型 suspend 操作。 Data 层不要再暴露 start\u002Fstop，用 Flow 接管生命周期。 Data 层不要再暴露 start\u002Fstop，用 Flow 接管生命周期。 如果这个场景不是 Flow，而是一个生命周期型 suspend 操作，它又应该如何响应取消？ 如果这个场景不是 Flow，而是一个生命周期型 suspend 操作，它又应该如何响应取消？ 让 coroutine 自己管理生命周期，而不是额外设计一套 start\u002Fstop 或 init\u002Fcancel。 这个 Repository 的 coroutine 到底属于谁？ 这个 Repository 的 coroutine 到底属于谁？ 生命周期已经存在于 coroutine 的父子关系里。 不要让调用方通过 start\u002Fstop 管理 Data 层的生命周期，让 Flow 接管它。 不要让调用方通过 start\u002Fstop 管理 Data 层的生命周期，让 Flow 接管它。 当一个操作本身适合用 suspend 表达时，也不应该重新退回到： 不需要手动取消，这就是 Structured Concurrency 的优势。 不需要手动取消，这就是 Structured Concurrency 的优势。 awaitCancellation() 只是技术手段。 而 Structured Concurrency 解决的是更大的问题： 如果它属于 viewModelScope，那么就让它成为 viewModelScope 的一部分。 “页面销毁的时候，我是不是还漏掉了一个资源释放？”","\u002Fapi\u002Fmedia\u002Fposts\u002Fandroid-suspend-repository-cancel-0742d78e\u002F3.webp",{"local":27},{"headline":30,"body":31,"imageUrl":32,"images":33},"“页面销毁的时候，我是不是还漏掉了一个资源释放？” 因为生命周期已经进入了 coroutine 的结构。 取消不是一个额外的业务 API，而是…","“页面销毁的时候，我是不是还漏掉了一个资源释放？” 因为生命周期已经进入了 coroutine 的结构。 取消不是一个额外的业务 API，而是 coroutine 生命周期自然产生的结果。 这就是 Structured Concurrency 真正漂亮的地方。","\u002Fapi\u002Fmedia\u002Fposts\u002Fandroid-suspend-repository-cancel-0742d78e\u002F4.webp",{"local":32},[],[],{"name":37,"url":38},"Juejin","https:\u002F\u002Fjuejin.cn\u002Fpost\u002F7690463587244687411","zh",{"handle":41,"displayName":42},"spots","Spots",null,{"views":45,"likes":46,"saves":46,"shares":46,"completions":46,"opens":46,"skips":46,"depthSum":46},3,0,"2026-09-28T17:52:57.835Z","local"]