SwiftUI NavigationLink推送视图时Published ViewModel泄漏问题
问题背景
此前围绕「异步回调获取数据后推送对应视图」的实现方案落地后,出现了不符合预期的内存泄漏问题。
项目采用SwiftUI + MVVM架构,设计逻辑为父ViewModel发布子ViewModel实例,供给对应弹出视图展示;预期弹出视图销毁时,对应ViewModel会被自动释放,但现有实现完全不符合该预期。
具体异常表现:UserView 被销毁后,其对应的 UserViewModel 实例仍驻留内存,UserViewModel 的 deinit 从未打印“Deinit UserViewModel”日志,仅当下次触发 pushUser 推送视图时,旧实例才会执行释放逻辑。
现有实现代码如下:
struct ParentView: View { @ObservedObject var vm: ParentViewModel var presentationBinding: Binding<Bool> { .init(get: { vm.pushUser != nil }, set: { isPresented in if !isPresented { vm.pushUser = nil } } ) } var body: some View { VStack { Button("Get user") { vm.getUser() } Button("Read user") { print(vm.pushUser ?? "No userVm") } if let userVm = vm.pushUser { NavigationLink( destination: UserView(vm: userVm), isActive: presentationBinding, label: EmptyView.init ) } } } } class ParentViewModel: ObservableObject { @Published var pushUser: UserViewModel? = nil var cancellable: AnyCancellable? private func fetchUser() -> AnyPublisher<User, Never> { Just(User.init(id: "1", name: "wiingaard")) .delay(for: .seconds(1), scheduler: DispatchQueue.main) .eraseToAnyPublisher() } func getUser() { cancellable = api.getUser().sink { [weak self] user in self?.pushUser = UserViewModel(user: user) } } } struct User: Identifiable { let id: String let name: String } class UserViewModel: ObservableObject, Identifiable { deinit { print("Deinit UserViewModel") } @Published var user: User init(user: User) { self.user = user } } struct UserView: View { @ObservedObject var vm: UserViewModel var body: some View { Text(vm.user.name) } }
销毁UserView后,通过Xcode内存图调试工具可观察到UserViewModel实例仍处于分配状态,其顶层引用为SwiftUI内部的AnyViewStorage类型对象,引用链路来自于包装了UserView的导航修饰符视图节点。
内存泄漏触发原因
- 现有实现采用的是带
isActive绑定的老式NavigationLink写法,且将该NavigationLink放在if let userVm = vm.pushUser的条件渲染分支内,以EmptyView作为标签。这种实现方式下,只要NavigationLink被渲染到视图树中,SwiftUI导航系统就会对其关联的destination视图(即UserView)产生强引用。 - 当用户触发返回逻辑时,绑定的presentationBinding会将
vm.pushUser置为nil,触发条件分支的视图移除逻辑,但SwiftUI导航层内部并不会同步清除之前缓存的目标视图引用,这部分缓存的强引用会一直持有UserView及其持有的UserViewModel,导致实例无法释放。 - 只有当
pushUser被再次赋值为新的UserViewModel时,新的条件分支会生成新的NavigationLink节点,替换掉导航层内部旧的缓存引用,旧的UserViewModel才会失去强引用、完成释放,这也是观察到“下次push时旧实例才执行deinit”现象的核心原因。
对应解决方案
- 适配iOS 16及以上版本时,替换基于
isActive绑定的老式NavigationLink逻辑,采用值驱动的navigationDestination路由写法:将pushUser作为路由的关联数据源,由SwiftUI导航系统统一管理目标视图的生命周期。当绑定的数据源被置为nil时,对应的目标视图与关联的ViewModel会被同步从导航栈移除、自动释放。 - 需要兼容iOS 15及更早版本时,调整
NavigationLink的创建方式,不要在条件分支内直接内嵌创建目标视图,改用NavigationLink(tag:selection:)的初始化形式,将可选的pushUser值作为选择绑定源。这种方式下SwiftUI不会提前强持有未触发跳转的目标视图,视图销毁时关联的ViewModel会随视图树的移除同步释放。 - 不需要额外在ViewModel层添加弱引用修饰:泄漏的根源是SwiftUI导航层的非预期强引用,而非ViewModel之间的引用循环,调整导航实现写法后,
UserView使用@ObservedObject持有UserViewModel的逻辑即可正常实现自动释放,不需要额外的手动内存管理操作。
内容的提问来源于stack exchange,提问作者Wiingaard
相关产品推荐
相关产品推荐

