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

SwiftUI中@Published属性为何残留内存痕迹?是否属于内存泄漏?(VIPER架构场景)

问题分析与解决方案

嘿,这个情况我在SwiftUI结合VIPER架构开发时也碰到过,咱们一步步拆解原因和解决办法:

核心原因

1. SwiftUI订阅机制的滞后清理

当你在View中通过@ObservedObject绑定Presenter时,SwiftUI会自动为@Published属性创建Combine订阅来监听状态变化。即使视图被关闭销毁,SwiftUI内部的订阅管理池可能不会立即清理这些订阅对应的键路径引用——尤其是当你直接访问presenter.isReminderActive这类属性时,SwiftUI会生成特定的键路径(比如\ExamplePresenter.isReminderActive)来监听,这些键路径的元数据可能会短暂残留在内存中,直到系统自动内存回收机制触发。

2. Combine Publisher的残留引用

@Published的底层依赖Combine框架的Publisher实现。如果Presenter在释放时,Combine的订阅没有被完全取消(比如SwiftUI的内部订阅没有及时收到完成信号),就会导致键路径相关的引用无法被立即回收。虽然Presenter实例本身已经被释放,但这些订阅关联的键路径信息可能会暂时留在内存中。

3. 视图层级的隐性缓存

有时候,UIHostingController被dismiss后,SwiftUI可能会暂时缓存视图相关的一些元数据(包括键路径),用于可能的快速重建。这种缓存通常是临时的,但在内存调试中会被检测到。

可行的解决方案

  • 手动触发订阅清理:在Presenter的deinit方法中,手动对所有@Published属性发送完成信号,帮助Combine清理订阅:

    extension ExamplePresenter {
        deinit {
            // 手动触发@Published属性的完成事件
            _datasource.send(completion: .finished)
            // 如果是嵌套的state对象,同样处理
            _states.send(completion: .finished)
        }
    }
    

    注:_datasource是@Published自动生成的底层Publisher变量,命名规则为下划线加属性名。

  • 用弱引用包装Presenter:在View中避免直接持有Presenter的强引用,改用弱引用包装器,让SwiftUI更容易清理订阅:

    struct WeakPresenter<T: ObservableObject> {
        weak var value: T?
        init(_ value: T) {
            self.value = value
        }
    }
    
    struct ExampleViewView<T: ObservablePresenter>: View where T.ViewStates == ExampleStates {
        @ObservedObject var weakPresenter: WeakPresenter<T>
        var body: some View {
            guard let presenter = weakPresenter.value else {
                return Color.clear // 处理Presenter已释放的边界情况
            }
            if !presenter.isReminderActive {
                Color.red
            }
        }
    }
    

    初始化View时传入WeakPresenter(presenter)即可。

  • 确保HostingController被完全释放:在presenting VC中,dismiss视图后手动置空UIHostingController的引用,避免隐性强引用:

    var viewController: ExampleUIHostingController?
    
    // 弹出视图
    func presentExampleView() {
        let presenter = ExamplePresenter()
        viewController = ExampleUIHostingController(rootView: ExampleSwiftUIView(presenter: presenter))
        present(viewController!, animated: true)
    }
    
    // 关闭视图后
    func dismissExampleView() {
        dismiss(animated: true) {
            self.viewController = nil // 手动置空,释放引用链
        }
    }
    

额外说明

这种键路径残留通常是暂时的,系统ARC会在后续内存回收周期中清理掉这些痕迹。如果内存调试显示的是长期残留,那大概率是订阅未被正确取消,或者存在隐性强引用链——可以用Xcode的内存图工具,查看键路径引用的持有者,进一步定位问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 15:39:09