SwiftUI NavigationLink 内存泄漏

14

我对SwiftUI的NavigationView堆栈中的内存管理工作方式有疑问。我有一个视图,在其中我声明了NavigationView和NavigationLink,导航功能良好,但当我从堆栈(例如向上的返回按钮)弹出视图时,控制台上没有打印销毁(deinit)信息,并且TestViewModel仍然可以在内存图中找到。当不再需要时,如何取消初始化我的TestViewModel?

    /// First view in application
    struct ContentView: View {

        var body: some View {
            NavigationView {
                VStack {
                    Text("Hello, leak!")
                    NavigationLink(
                        destination: TestView(viewModel: TestViewModel()),
                        label: { Text("Create leak ‍♂️") }
                    )
                }
            }
        }
    }

    /// Just simple class for init and deinit print
    class TestViewModel: ObservableObject {

        @Published var text = "Test"

        init() {
            print("TestViewModel init")
        }

        deinit {
            print("TestViewModel deinit")
        }
    }

    /// Second view, which is poped from stack
    private struct TestView: View {

        @ObservedObject var viewModel: TestViewModel

        var body: some View {
            Text(viewModel.text)
        }
    }

更新 添加了内存图截图,我之前忘记了。

内存图截图底部

内存图截图顶部

更新

在真实设备上测试,导航正常工作。看起来,当弹出视图时,视图模型没有被销毁,但在下一次推入视图时重新初始化了。但问题仍然存在,有没有办法在导航栈中弹出视图时销毁视图模型?

TestViewModel init
TestViewModel deinit
TestViewModel init

另外,当我将另一个视图添加到堆栈中时,行为会有所改变。现在,第二个视图的视图模型将导致泄漏,但是第一个视图将按预期被取消初始化。

First view push
TestViewModel init
Second view push
TestViewModel2 init
Second view pop
First view pop
TestViewModel deinit

不需要管理deinit,但我认为框架会按照自己的意愿进行操作,我们无法真正控制。 - LetsGoBrandon
不幸的是,我还没有在真实设备上测试过它,而且我不能使用NavigationLink两次,因为在iOS模拟器上存在一个错误。一旦我在真实设备上测试过它,我会更新我的答案。尽管如此,我发现很奇怪,我的ARC不会释放这个对象。 - Róbert Oravec
3
我想,玩了一会儿后,我大概明白了为什么内存没有被释放。在使用SwiftUI时,我们将整个应用程序包装在一个单独的托管控制器中,这可能意味着我们只有一个范围,整个应用程序是一个单一状态(我一直无法理解它)。每个类引用将由此控制器持有,直到被另一个引用替换(如上所述)。我认为,苹果应该提供某种方式手动或自动管理这些作用域,在执行某种导航时。 - Róbert Oravec
确保你的viewModel是@StateObject,这将创建一个可管理的实例。 - OhadM
3个回答

22

我曾经也遇到同样的问题,花费了很长时间才找到解决方法。最终,我成功了!使用 .navigationViewStyle(StackNavigationViewStyle())。将其作为 NavigationView 的一个函数添加:

NavigationView {
   ...
}
.navigationViewStyle(StackNavigationViewStyle())

为什么这个方法可以解决问题,你有什么想法吗? - Chris Prince
结果发现,当将新视图推入导航堆栈时,此解决方案会破坏动画过渡。 :( - Wiingaard
10
每次我开始想“哇,SwiftUI确实让事情变得容易”时,我总是会遇到一些愚蠢而隐晦的问题,需要花费两天时间去追踪。但感谢您结束了我两天的搜索。 - Mike
3
对我没用。 - Gusev Andrey
2
@Chris, 默认的导航样式是ColumnNavigationViewStyle,当尺寸类别为常规时,它会显示列表和选定的详细项目。如果在iPhone Pro Max上以纵向方式选择一个项目,然后返回,但旋转设备仍然可以在第二列中看到所选项目。因此,ColumnNavigationViewStyle会记住所选项目,直到选择新项目。 - Michael Long
显示剩余2条评论

6
当我观看SwiftUI中的数据要素时,我想我找到了我的问题的答案。这是新的StateObject属性包装器(我找不到文档,但这里有文章描述它)。现在,当我希望我的数据仅存在于视图范围内而不进行任何黑客操作时,我可以使用@StateObject。

1
我有同样的问题,但是我不太理解你的解释。你能提供一些代码吗?它是如何解决你的问题的?每次从栈中弹出视图时,TestViewModel现在是否已从内存中释放? - Roman
1
如果一个视图模型具有需要从View init方法传入的参数,我认为无法使用@StateObject。 - Chris Prince
非常有帮助! - nomnom

1
这是因为默认导航样式为 ColumnNavigationViewStyle,每当水平尺寸类别为常规时,它都会显示导航列表和所选详细项。
要在iPhone Pro Max上查看此效果,请运行应用程序。然后,在纵向模式下选择一个项目,返回,然后旋转设备。 Bingo。您将在第二列中看到所选项目。
因此,为了使此功能生效,ColumnNavigationViewStyle将记住所选项目,直到选择新项目。
这反过来会导致神秘的“保留周期”。它不是泄漏,而只是它的工作方式。(即使在像 iPhone mini 这样永远不会出现第二列的设备上也是如此。)
.navigationViewStyle(StackNavigationViewStyle()) 修复方法在其他地方提到并更改了此行为。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接