使用 NavigationLink 推送视图时,已发布的 ViewModel 导致内存泄漏

Published ViewModel causes memory leak when pushing a view with NavigationLink

我之前问过 如何使用从异步回调接收到的数据推送视图。我最终使用的方法结果导致内存泄漏。

我正在尝试使用 SwiftUI 的 MVVM 构建我的应用程序,因此 ViewModel 应该发布另一个 ViewModel,然后 View 知道如何在屏幕上呈现。一旦呈现的视图从屏幕上消失,我希望相应的 ViewModel 被取消初始化。但是,提议的解决方案绝非如此。

UserView 被关闭后,我最终在内存中泄漏了一个 UserViewModel 的实例。 UserViewModel 从不打印 "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 并检查调试内存图后,我看到 UserViewModel 的一个实例仍在分配中。

顶级引用 (view.content.vm) 具有种类:(AnyViewStorage in fff57ab1a78)<ModifiedContent<UserView, (RelationshipModifier in fff57ad2760)<String>>> 和层次结构:SwiftUI.(AnyViewStorage in fff57ab1a78)<SwiftUI.ModifiedContent<MyApp.UserView, SwiftUI.(RelationshipModifier in fff57ad2760)<Swift.String>>> AnyViewStorageBase _TtCs12_SwiftObject

是什么原因导致此内存泄漏,我该如何删除它?

请参考这个post (),它通过在 NavigationView 上添加解决了 SwiftUI 中的内存泄漏问题:

.navigationViewStyle(StackNavigationViewStyle())

但是它破坏了动画,这个问题有一个 hacky 解决方案。出现动画问题是因为“if let”中的可选链接。

当在 NavigationLink 中将“nil”设置为目的地时,即使“presentationBinding”为真,它也基本上不会去任何地方。

我邀请您尝试这段代码,因为它修复了由 StackNavigationViewStyle 导致的动画问题(并且没有内存泄漏):

虽然不如可选链接那么漂亮,但它完成了工作。

如果您在 View 中使用 @State 并在 ViewModel 中收听 @Publisher,我可以看到 ViewModel 是 deinit()。

示例:

@State var showTest = false

 NavigationLink(destination: SessionView(sessionViewModel: outgoingCallViewModel.sessionViewModel),
                           isActive: $showTest,
                           label: { })
                .isDetailLink(false)
        )
        .onReceive(viewModel.$showView, perform: { show in
            if show {
                showTest = true
            }
        })

如果您在 NavigationLink 中将 viewModel.$show 用作 isActive,则 viewModel 永远不会 deinit()。