You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift Combine:借助Coordinator模式与订阅管理避免内存泄漏

用Combine替代Coordinator模式代理时的内存泄漏问题解决方法

问题描述

我在实现Coordinator模式时尝试用Combine替代代理,但从childCoordinators列表移除协调器后出现了内存泄漏。如何确保协调器能自动释放?具体来说,我希望coordinator.actions.sink订阅能被正确终止,协调器能从内存中被清理。

相关代码:

// MARK: - BaseCoordinator
@MainActor
class BaseCoordinator: Coordinator {
    let navigator: NavigatorType
    var childCoordinators: [Coordinator] = []
    var cancellables = Set<AnyCancellable>()

    init(navigator: NavigatorType) {
        self.navigator = navigator
    }

    func start() {
        // Default implementation, can be overridden by subclasses
    }
}


// MARK: - ExampleFlowCoordinator
final class ExampleFlowCoordinator: BaseCoordinator {
    override func start() {
        let coordinator = ExampleCoordinator()
        add(coordinator)
        coordinator.actions.sink { [weak self] action in
            guard let self else { return }
            switch action {
            case .done:
                remove(coordinator)
            }
        }
        .store(in: &cancellables)

        navigator.setRootViewController(coordinator.rootViewController, animated: true)

        coordinator.start()
    }

    private func showExample2() {
        let coordinator = ExampleCoordinator2()

        coordinator.actions.sink { [weak self] action in
            guard let self else { return }
            switch action {
            case .done:
                remove(coordinator)
            }
        }
        .store(in: &cancellables)

        add(coordinator)
        coordinator.start()
        navigator.present(coordinator.rootViewController, animated: true) { [weak self] in
            guard let self else { return }
            remove(coordinator)
        }
    }
}

补充:在showExample2()函数中,执行remove(coordinator)后,协调器已成功从childCoordinators中移除,但由于coordinator.actions.sink的存在,协调器仍驻留在内存中(移除该订阅代码后,内存释放正常)。

核心问题分析

内存泄漏的根源是父协调器的cancellables集合持有了订阅,而订阅又捕获了子协调器的强引用——即使从childCoordinators移除子协调器,订阅的强引用依然让子协调器无法被ARC回收。

解决方法

方法1:让子协调器自行管理订阅

将订阅生命周期与子协调器绑定,当子协调器被释放时,订阅自动取消。

修改子协调器(以ExampleCoordinator2为例):

final class ExampleCoordinator2: BaseCoordinator {
    let actions = PassthroughSubject<Example2Action, Never>()
    private var ownCancellables = Set<AnyCancellable>() // 子协调器专属的订阅集合

    // 对外暴露订阅方法,将订阅存储到自己的集合中
    func subscribeToActions(_ handler: @escaping (Example2Action) -> Void) {
        actions.sink(receiveValue: handler)
            .store(in: &ownCancellables)
    }
}

父协调器调用方式调整:

private func showExample2() {
    let coordinator = ExampleCoordinator2()
    // 调用子协调器的订阅方法
    coordinator.subscribeToActions { [weak self] action in
        guard let self else { return }
        switch action {
        case .done:
            remove(coordinator)
            // 无需手动取消订阅,子协调器释放时会自动清理ownCancellables
        }
    }
    add(coordinator)
    coordinator.start()
    navigator.present(coordinator.rootViewController, animated: true) { [weak self] in
        guard let self else { return }
        remove(coordinator)
    }
}

方法2:父协调器关联订阅与子协调器

在父协调器中用字典存储子协调器对应的订阅,移除子协调器时同步取消订阅。

修改BaseCoordinator:

@MainActor
class BaseCoordinator: Coordinator {
    let navigator: NavigatorType
    var childCoordinators: [Coordinator] = []
    private var coordinatorSubscriptions: [Coordinator: AnyCancellable] = [:] // 新增:存储订阅与子协调器的映射

    init(navigator: NavigatorType) {
        self.navigator = navigator
    }

    func start() {
        // Default implementation, can be overridden by subclasses
    }

    // 新增:存储子协调器的订阅
    func storeSubscription(for coordinator: Coordinator, subscription: AnyCancellable) {
        coordinatorSubscriptions[coordinator] = subscription
    }

    // 重写remove方法,移除时同步取消订阅
    func remove(_ coordinator: Coordinator) {
        childCoordinators.removeAll { $0 === coordinator }
        coordinatorSubscriptions.removeValue(forKey: coordinator) // 取消订阅,释放强引用
    }
}

父协调器showExample2()调整:

private func showExample2() {
    let coordinator = ExampleCoordinator2()
    // 创建订阅并存储到映射字典
    let subscription = coordinator.actions.sink { [weak self] action in
        guard let self else { return }
        switch action {
        case .done:
            remove(coordinator)
        }
    }
    storeSubscription(for: coordinator, subscription: subscription)
    
    add(coordinator)
    coordinator.start()
    navigator.present(coordinator.rootViewController, animated: true) { [weak self] in
        guard let self else { return }
        remove(coordinator)
    }
}

方法3:利用Combine自动取消的特性

如果actions事件可以直接绑定到父协调器的方法,可结合弱引用使用sink,配合方法2的映射存储,确保订阅能随子协调器移除而取消。

// 父协调器新增事件处理方法
private func handleExample2Action(_ action: Example2Action, coordinator: ExampleCoordinator2) {
    switch action {
    case .done:
        remove(coordinator)
    }
}

// 订阅时的写法
private func showExample2() {
    let coordinator = ExampleCoordinator2()
    let subscription = coordinator.actions
        .sink { [weak self] action in
            self?.handleExample2Action(action, coordinator: coordinator)
        }
    storeSubscription(for: coordinator, subscription: subscription)
    
    add(coordinator)
    coordinator.start()
    navigator.present(coordinator.rootViewController, animated: true) { [weak self] in
        guard let self else { return }
        remove(coordinator)
    }
}

内容的提问来源于stack exchange,提问作者Shape Fit

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 06:45:12