清空NavigationStack path后对象仍驻留内存的原因排查
我正在实现一个用于管理流程的Coordinator,状态存储在CoordinatorStore中,该类包含两个@Published属性:screen用于控制当前显示的View,path用于控制导航栈的内容。
执行操作流程:showA→showB→showInitial→Go to Stack后,我原本认为由于持有StoreA和StoreB实例的path已被清空,这两个对象会被内存释放,但实际并未发生。重复执行该操作流程后,内存中会累积多个StoreA和StoreB实例,请问问题出在哪里?
核心定义代码
enum Path: Hashable { case a(StoreA) case b(StoreB) } enum Screen { case initial case stack } final class CoordinatorStore: ObservableObject { @Published var path: [Path] = [] @Published var screen: Screen = .stack func showA() { let store = StoreA() path.append(.a(store)) } func showB() { let store = StoreB() path.append(.b(store)) } func showInitial() { path = [] screen = .initial } func showStack() { screen = .stack } }
Coordinator视图代码
struct Coordinator: View { @ObservedObject var store: CoordinatorStore var body: some View { switch store.screen { case .initial: initial case .stack: stack } } var stack: some View { NavigationStack(path: $store.path) { VStack { Text("Root") } .toolbar { Button(action: self.store.showA) { Text("Push A") } } .navigationDestination(for: Path.self) { path in switch path { case .a(let store): ViewA(store: store) .toolbar { Button(action: self.store.showB) { Text("Push B") } } case .b(let store): ViewB(store: store) .toolbar { Button(action: self.store.showInitial) { Text("Show Initial") } } } } } } var initial: some View { VStack { Text("Initial") Button(action: store.showStack) { Text("Go to Stack") } } } }
ViewA与StoreA代码
struct ViewA: View { @ObservedObject var store: StoreA var body: some View { Text("View A") } } final class StoreA: NSObject, ObservableObject { deinit { print("Deinit: \(String(describing: self))") } }
ViewB与StoreB代码
struct ViewB: View { @ObservedObject var store: StoreB var body: some View { Text("View B") } } final class StoreB: NSObject, ObservableObject { deinit { print("Deinit: \(String(describing: self))") } }
问题出在NavigationStack的内部缓存机制:当切换到.initial视图时,NavigationStack会被从视图树中移除,但SwiftUI会对之前的导航栈相关视图(包括对应的Store实例)进行缓存,以便快速恢复。即使清空了path,这个内部缓存仍然持有StoreA和StoreB的引用,导致它们无法被释放。
另外,Path枚举直接持有Store实例,而NavigationStack在处理导航目标时会保留这些枚举值的引用,加上缓存机制,进一步延长了对象的生命周期。
有两种可行的解决方式:
方式一:使用标识而非直接持有实例
修改Path枚举,让它只存储标识信息,在需要创建视图时再初始化对应的Store:
enum Path: Hashable { case a case b } // 修改Coordinator中的navigationDestination部分 .navigationDestination(for: Path.self) { path in switch path { case .a: ViewA(store: StoreA()) .toolbar { Button(action: self.store.showB) { Text("Push B") } } case .b: ViewB(store: StoreB()) .toolbar { Button(action: self.store.showInitial) { Text("Show Initial") } } } }
这种方式下,Store实例的生命周期完全由对应的View管理,当View被销毁时,Store也会被释放。
方式二:手动清除NavigationStack的缓存
在切换到.initial视图时,通过给NavigationStack添加一个id来重置它的状态,让SwiftUI完全重建导航栈,从而清除之前的缓存:
// 在CoordinatorStore中添加 @Published var navigationStackId = UUID() func showInitial() { path = [] screen = .initial navigationStackId = UUID() // 重置id } // 在stack视图中使用 NavigationStack(path: $store.path) { // 原有内容不变 } .id(store.navigationStackId)
这样每次返回初始视图再进入栈视图时,NavigationStack会被完全重建,之前的缓存会被清理,Store实例就能正常释放。
内容的提问来源于stack exchange,提问作者hydro1337x

