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打印,确认是否被正确销毁:
如果未打印日志,说明存在强引用循环,可通过Xcode的Memory Graph Debugger定位引用链。deinit { print("SampleVC deinit") } - 检查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
相关产品推荐
相关产品推荐

