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

UIKit嵌入SwiftUI时API调用与导航引发的内存泄漏问题

UIKit嵌入SwiftUI视图的内存泄漏排查与解决

问题场景

在基于UIKit的应用中嵌入SwiftUI视图,反复执行视图控制器的推入和弹出操作时,内存占用持续增加,存在内存泄漏。已尝试添加[weak self]、检查SwiftUI视图强引用循环、弹出时移除hosting视图等操作,问题仍未解决。

简化代码结构

protocol SampleDelegate: AnyObject {
    func push()
    func pop()
}
class SampleVC: UIViewController, SampleDelegate {
    var viewModel: SampleVM!
    override func viewDidLoad() {
        super.viewDidLoad()
        let  swiftUIView = SampleView(viewModel: viewModel)
        let hostingController = UIHostingController(rootView: swiftUIView)
        self.addChild(hostingController)
        self.view.addSubview(hostingController.view)
        hostingController.didMove(toParent: self)
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        // Set up Auto Layout constraints for the SwiftUI view
        NSLayoutConstraint.activate([
            hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
            hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        // Use of viewmodel for making api controll on viewcontroller (UiKit)
        self.bindData()
    }
    func bindData() {
        viewModel.error.bind { error in
            Toast.showError(error: error ?? NetworkError.somethingWentWrong)
        }
    }
    func push() {
        // push new viewcontroller
    }
    
    func pop() {
        // pop viewcontroller
    }
}
class SampleVM: ObservableObject {
    @Published var fetchCompletion = false
    var error: Observable<Error?> = Observable(nil)
    
    func fetchData() {
        // call api to fetch and change fetched
    }
}

struct SampleView: View {
    @ObservedObject var viewModel: SampleVM
    weak var delegate: SampleDelegate?
    init(viewModel: SampleVM, delegate: SampleDelegate? = nil) {
        self.viewModel = viewModel
        self.delegate = delegate
    }
    var body: some View {
        VStack {
            Text("\((viewModel.fetchCompletion == true) ? "fetching" : "fetched")")
            Spacer()
            HStack {
                Button {
                    delegate?.pop()
                } label: {
                    Text("pop")
                }
                Button {
                    delegate?.push()
                } label: {
                    Text("push")
                }
            }
        }
    }
}

排查与解决思路

  • 修复HostingController生命周期管理:当前代码中hostingController是viewDidLoad内的局部变量,无法在VC销毁时移除。需将其改为SampleVC的实例变量,并在deinit中正确清理:
    class SampleVC: UIViewController, SampleDelegate {
        var viewModel: SampleVM!
        private var hostingController: UIHostingController<SampleView>?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            let swiftUIView = SampleView(viewModel: viewModel, delegate: self)
            hostingController = UIHostingController(rootView: swiftUIView)
            // ... 原有添加视图和约束代码 ...
        }
        
        deinit {
            hostingController?.willMove(toParent: nil)
            hostingController?.view.removeFromSuperview()
            hostingController?.removeFromParent()
        }
    }
    
  • 解决自定义Observable的订阅泄漏:SampleVM中自定义的Observable如果没有提供取消订阅的机制,会导致SampleVC被长期持有。建议改用Combine框架的原生类型管理订阅:
    class SampleVM: ObservableObject {
        @Published var fetchCompletion = false
        let error = PassthroughSubject<Error?, Never>()
        
        func fetchData() {
            // API调用完成后发送错误:error.send(...)
        }
    }
    
    class SampleVC: UIViewController, SampleDelegate {
        private var cancellables = Set<AnyCancellable>()
        
        func bindData() {
            viewModel.error
                .sink { error in
                    Toast.showError(error: error ?? NetworkError.somethingWentWrong)
                }
                .store(in: &cancellables)
        }
    }
    
  • 验证对象销毁状态:给SampleVC、SampleVM添加deinit打印,确认是否被正确销毁:
    deinit {
        print("SampleVC deinit")
    }
    
    如果未打印日志,说明存在强引用循环,可通过Xcode的Memory Graph Debugger定位引用链。
  • 检查API请求的内存泄漏:确保fetchData中的API请求在VC销毁时被取消,比如用URLSessionDataTask的cancel()方法,或在闭包中使用[weak self]避免持有VC。
  • 明确SwiftUI视图的Delegate传递:初始化SampleView时需明确传入self作为delegate,避免隐式引用问题:
    let swiftUIView = SampleView(viewModel: viewModel, delegate: self)
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 00:44:56