13-23.【Combine】全局 State / Environment 中使用 Combine 时,如何避免反向依赖和意外订阅?

4 阅读3分钟

在全局状态(Global State)或环境对象(Environment)中深度集成 Combine 时,最核心的风险在于**“状态爆炸”“订阅管理失控”**。当多个解耦的模块通过单例或 Environment 共享同一个 Publisher 时,很容易形成循环依赖,或因未及时取消订阅而导致内存泄漏。

以下是规避反向依赖和意外订阅的架构方案:


1. 严格的单向依赖:接口隔离(Interface Segregation)

避免将整个 GlobalStore 直接暴露给所有模块。如果模块 A 只需要 UserStatus,就不应该让它感知 OrderHistory

  • 解决方案:使用协议(Protocol)或瘦身版的代理对象,隐藏底层的 Subject
  • 防御技巧:不要在 Environment 中传递 CurrentValueSubject,而应传递其经过 eraseToAnyPublisher() 转换后的只读版本。

Swift

protocol UserStateProvider {
    var userStatus: AnyPublisher<Status, Never> { get }
}

// 模块只能拿到受限的 Provider,无法调用 .send()
func setupModule(with provider: UserStateProvider) { ... }

2. 避免反向依赖:Action 路由而非直接调用

当子模块需要修改全局状态时,常见的错误是让子模块持有 GlobalStore 并直接调用其方法,这形成了双向依赖

  • 解决方案:使用命令式分发(Dispatch) 。子模块只负责发送一个“意图(Action)”,由一个顶层的协调器或 Store 监听这些 Action 并更新状态。
  • 底层机制:子模块持有一个 PassthroughSubject<Action, Never> 的引用。它只管往里“扔”东西,不关心谁处理,也不关心全局状态如何变化。

3. 规避意外订阅:生命周期绑定(Owner-based Lifetime)

全局状态通常是常驻内存的,如果订阅逻辑(.sink)不当,会导致原本该销毁的视图对象被全局闭包强引用。

  • 解决方案:Cancellable 的局部化

    • 绝不要将视图级别的 AnyCancellable 存储在全局单例中。
    • 严格使用 [weak self] :在任何订阅全局状态的 sink 闭包中,必须弱引用 self
  • 自动取消:利用 SwiftUI 的 .onReceive.task。这些视图修饰符会在视图销毁时自动切断 Combine 订阅,无需手动管理 Set<AnyCancellable>


4. 颗粒度控制:防抖与去重(Performance Guard)

全局状态的任何微小变动都可能触发全 App 的订阅回调。如果不加控制,意外的订阅刷新会拖慢整个系统。

  • 解决方案:在全局 Publisher 后紧跟 .removeDuplicates()

  • 高级技巧:针对特定视图,使用 map 提取最小必要的属性,并使用 removeDuplicates

    Swift

    // 只有当积分变化时,积分图标才刷新,即使用户名、头像等其他全局状态在变
    environment.globalState
        .map { $0.userPoints }
        .removeDuplicates()
        .receive(on: DispatchQueue.main)
        .assign(to: &$points)
    

5. 调试意外订阅:使用标记算子

当你怀疑某个全局状态被意外频繁触发时,可以使用 Combine 内置的调试工具:

  • .print("Debug Label") :在控制台打印每次值的产生、订阅和取消。
  • .handleEvents:在订阅发生时打印堆栈信息。

Swift

globalPublisher
    .handleEvents(receiveSubscribe: { print("新订阅者来自: (Thread.callStackSymbols[1])") })
    .sink { ... }

架构决策矩阵

风险点预防措施预期效果
反向依赖引入 Action 枚举与 Dispatcher模块间物理隔离,支持独立编译
内存泄漏闭包强使用 [weak self] + 局部 AnyCancellable视图销毁时订阅立即终止
性能抖动强制在全局出口处 removeDuplicates减少不必要的 UI 重绘
非法修改外部仅暴露 AnyPublisher,隐藏 Subject状态变更路径唯一可追溯