SwiftUI (Mac) @Observable 内存泄漏,与数组相关,触发 App 生命周期

问题描述 投票:0回答:1

@Observable
视图模型与数组集合一起使用,如果在 init 中解决,将在突变时触发第二个应用程序生命周期。

分解:
视图模型

@Observable
class ContentViewModel {
    var items = [String]()
    
    init() {
        print("Init")
        if items.isEmpty {}
    }

    deinit {
        print("deinit")
    }

    
    func update() {
        items.append("New")
    }
}

查看

struct ContentView: View {
    
    @State var viewModel: ContentViewModel = .init()
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Button("Update", action: viewModel.update)
        }
        .padding()
    }
}

运行此程序时,它将打印“Init”(当然是预期的),但单击“Update”会触发完整的生命周期。
它将打印另一个“Init”。在(单个)

WindowGroup
上设置断点将在那里停止。这意味着(我认为)整个
var body: some Scene
正在被触发。

  • 只需删除
    items.isEmpty
    ,问题就消失了。
  • 内存图表显示
    ContentViewModel
    两次。
  • 没有 deinit。
  • 仅在使用
    @Observable
    时发生(如果使用
    ObservableObject/ObservedObject
    则不会发生)
swiftui memory-leaks
1个回答
0
投票

@StateObject
@State
之间的主要区别在于,每次调用视图的初始化程序时都会调用
@State
的属性初始化程序。

因此,在首次初始化

viewModel
后,每次随后调用
ContentViewModel
时,最终都会创建
ContentView.init
的临时实例。

通常,临时实例会立即取消初始化,但如果您在

@Observable
类的初始化程序中执行繁重的操作,它仍然会影响性能。
State
的文档中指出了这一点。

当 SwiftUI 时,

A

State
属性总是实例化其默认值 实例化视图。因此,要避免副作用并 初始化默认值时的性能密集型工作。为了 例如,如果视图频繁更新,则分配新的默认对象 每次视图初始化都会变得昂贵。相反,您可以 使用
task(priority:_:)
推迟对象的创建 修饰符,仅在视图首次出现时调用一次:

struct ContentView: View {
    @State private var library: Library?


    var body: some View {
        LibraryView(library: library)
            .task {
                library = Library()
            }
    }
}

有时,由于 SwiftUI 创建依赖图的算法中的一些怪癖,会保留一个额外的实例。根据我的经验,每个

@State
我最多只看到一个额外实例。

在您的情况下,行

if items.isEmpty {}
访问观察跟踪属性
items
,并且此访问已在
observationRegistrar
中注册。这会以某种方式影响 SwiftUI 的依赖关系图,并且实例不会被释放。

您可以通过执行文档建议的操作来解决此问题 - 将其设置为可选并在

task
onAppear
中初始化它。

或者,您可以通过 malhal 使用 这个属性包装器

@MainActor
@propertyWrapper struct StateObservable<T>: DynamicProperty where T: Observable & AnyObject {
    @StateObject var storage: Storage
    
    class Storage: ObservableObject {
        @Published var object: T // @Published allows the object to be replaced and body will be called
        init(object: T) {
            self.object = object
        }
    }
    
    var wrappedValue: T {
        get {
            return storage.object
        }
        nonmutating set {
            storage.object = newValue
        }
    }
    
    // this allows $ syntax to be used in the same View where the wrapper is declared.
    var projectedValue: Bindable<T> {
        get {
            Bindable(storage.object)
        }
    }
    
    @inlinable public init(wrappedValue thunk: @autoclosure @escaping () -> T) {
        _storage = StateObject(wrappedValue: Storage(object: thunk()))
    }
}

此属性包装器将属性初始值设定项表达式包装到

@autoclosure
中,因此它的作用与
@StateObject
类似。

© www.soinside.com 2019 - 2024. All rights reserved.