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

SwiftUI+UIKit混合导航下ObservedObject未释放,是否存在内存泄漏?

问题

我正在开发一款SwiftUI与UIKit混合的应用:用UIKit实现主控制器和导航逻辑,SwiftUI负责界面设计。应用运行正常,但我担心存在内存泄漏——视图消失后,用于视图间传值的ViewModel并未调用deinit方法。我知道SwiftUI视图不会立即销毁,但因为用了UIKit导航,不清楚问题根源。

相关代码

单个用户的视图模型(UserViewModel)

// 获取到的每个用户对应的ViewModel
internal class UserViewModel: ObservableObject, Identifiable {
    
    // MARK: - 属性
    var currentListener: ListenerRegistration?
    
    @Published var request: Request?
    @Published var user: User
    
    init(user: User) {
        self.user = user
        getRequest()
        fetchAdmins()
    }
    
    deinit {
        // 这里从未被调用,无法执行removeListener()
    }
    
    func getRequest() {
        guard let uid = Auth.auth().currentUser?.uid else { return }
        guard let id = id else { return }
        
        self.currentListener = Collections.requests(id).document(uid).addSnapshotListener { snapshot, error in
            
            if let error = error {
                print(error.localizedDescription)
                return
            }
            
            if snapshot?.exists == true {
                if let request = try? snapshot!.data(as: Request.self) {
                    DispatchQueue.main.async {
                        self.request = request
                    }
                }
            }
        }
    }
    
    func removeListener() {
        self.currentListener?.remove()
    }
}

管理所有用户ViewModel的视图模型(UsersViewModel)

// 获取所有用户ViewModel的ViewModel
class UsersViewModel: ObservableObject {
    @Published var users = [UserViewModel]()
    
    func fetch() {
        DispatchQueue.global(qos: .background).async {
            Collections.users.getDocuments(completion: { snapshot, err in
                
                guard let documents = snapshot?.documents else { return }
                let users = documents.compactMap({ try? $0.data(as: User.self) })
                
                users.forEach { user in
                    let vm = UserViewModel(user: user)
                    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                        self.users.append(vm)
                    }
                }
            })
        }
    }
}

SwiftUI展示视图(HomeView)

// 展示用户单元格的视图
struct HomeView: View {
    @ObservedObject var usersViewModels: UsersViewModel
    
    // MARK: - 初始化
    init() {
        self.usersViewModels = UsersViewModel()
    }
    
    var body: some View {
        ListView(content: {
            ForEach(usersViewModels.users) { userViewModel in
                UserCell(viewModel: userViewModel).id(userViewModel.user.id)
            }
        })
    }
}

UIKit导航实现方法

public static func push<Content: View>(view: Content) {
    DispatchQueue.main.async {
        guard let tabBarController = UIApplication.rootViewController as? UITabBarController,
              let navigationController = tabBarController.selectedViewController as? UINavigationController else { return }

        navigationController.pushViewController(HostingController(content: view), animated: true)
    }
}

请问这种导航方式是否会引发内存问题?为何每次关闭页面后应用内存仅增长不下降?


分析与解决方案

你的UIKit导航方式本身不会直接导致内存泄漏,但代码中存在几个关键问题,共同导致ViewModel无法释放、内存持续增长:

1. Firebase快照监听器引发强引用循环

UserViewModel中的snapshotListener闭包默认会强引用self,而currentListener又持有该闭包,形成循环引用链:
UserViewModel → currentListener → 闭包 → UserViewModel
这直接导致deinit方法无法被调用,监听器始终存活,ViewModel也无法被系统回收。

修复方案:
在闭包中使用weak self打破循环引用,同时在deinit中主动移除监听器:

func getRequest() {
    guard let uid = Auth.auth().currentUser?.uid else { return }
    guard let id = id else { return }
    
    // 添加[weak self]避免循环引用
    self.currentListener = Collections.requests(id).document(uid).addSnapshotListener { [weak self] snapshot, error in
        guard let self = self else { return }
        
        if let error = error {
            print(error.localizedDescription)
            return
        }
        
        if snapshot?.exists == true {
            if let request = try? snapshot!.data(as: Request.self) {
                DispatchQueue.main.async {
                    self.request = request
                }
            }
        }
    }
}

deinit {
    removeListener()
    print("UserViewModel deinitialized")
}

2. UsersViewModel的生命周期未正确绑定

HomeView中直接在init方法内创建UsersViewModel并赋值给@ObservedObject,但@ObservedObject不会自动管理视图模型的生命周期。当HomeView对应的HostingController被弹出导航栈后,UsersViewModel可能因未取消的异步任务(比如延迟1秒的asyncAfter)被强引用,无法释放。

修复方案:

  • 将@ObservedObject替换为@StateObject(iOS 14+),@StateObject会确保视图模型与视图生命周期绑定,视图销毁时视图模型也会被正确回收:
struct HomeView: View {
    // 用@StateObject替代@ObservedObject管理生命周期
    @StateObject var usersViewModels = UsersViewModel()
    
    var body: some View {
        ListView(content: {
            ForEach(usersViewModels.users) { userViewModel in
                UserCell(viewModel: userViewModel).id(userViewModel.user.id)
            }
        })
        .onAppear {
            usersViewModels.fetch()
        }
    }
}
  • 在UsersViewModel中管理延迟任务,在deinit中取消所有未完成的异步操作:
class UsersViewModel: ObservableObject {
    @Published var users = [UserViewModel]()
    private var workItems = [DispatchWorkItem]()
    
    func fetch() {
        DispatchQueue.global(qos: .background).async { [weak self] in
            guard let self = self else { return }
            Collections.users.getDocuments(completion: { snapshot, err in
                
                guard let documents = snapshot?.documents else { return }
                let users = documents.compactMap({ try? $0.data(as: User.self) })
                
                users.forEach { user in
                    let vm = UserViewModel(user: user)
                    let workItem = DispatchWorkItem {
                        self.users.append(vm)
                    }
                    self.workItems.append(workItem)
                    DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: workItem)
                }
            })
        }
    }
    
    deinit {
        // 取消所有未执行的延迟任务
        workItems.forEach { $0.cancel() }
        print("UsersViewModel deinitialized")
    }
}

3. 导航逻辑的潜在优化

你的UIKit导航方法本身没有问题,但需要注意:

  • 确保HostingController被弹出导航栈时,没有全局单例或其他长期存活对象持有其引用。
  • 避免在SwiftUI视图中持有UIKit控制器的强引用,防止生命周期混乱。

总结

内存持续增长的核心原因是强引用循环(尤其是Firebase监听器闭包)和ViewModel生命周期未正确绑定,与UIKit导航方式本身无关。按照上述方案修复后,ViewModel的deinit方法会正常调用,页面关闭后内存也会逐步被系统回收。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 23:40:32