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

