My app is leaking model objects because it the objects are keeping closures that are retaining the view itself. It's better to show by an example. In the code below, Model
is not deallocated after the ContentView
disappears.
//
// Content View is an owner of `Model`
// It passes it to `ViewB`
//
// When button is tapped, ContentView executes action
// assigned to Model by the ViewB
//
struct ContentView: View {
@StateObject private var model = Model()
var body: some View {
VStack {
Button(action: {
model.action?()
}) {
Text("Tap")
}
ViewB(model: model)
}
.frame(width: 100, height: 100)
.onDisappear {
print("CONTENT DISAPPEAR")
}
}
}
struct ViewB: View {
@ObservedObject var model: Model
var body: some View {
Color.red.frame(width: 20, height: 20)
.onAppear {
//
// DANGER:
// Assigning this makes a leak and Model is never deallocated.
// This is because the closure is retaining 'self'
// But since it's a struct, how can we break the cycle here?
//
model.action = { bAction() }
}
}
private func bAction() {
print("Hey!")
}
}
class Model: ObservableObject {
var action: (() -> Void)?
deinit {
print("MODEL DEINIT")
}
}
I'm not sure why there's some kind of retain cycle occurring here. Since View is a struct, referencing it in a closure should be safe, right?
Model
is not a struct, it is an ObservableObject
which is of type AnyObject
which is an Object
you should apply weak to in the capture list for .onAppear
.onAppear { [weak model] }
I think you could also just capture model incase its self that the issue is on
.onAppear { [model] }
Ahoy @msmialko, while I can't give much reasoning for what I've observed, hopefully this will be a step in the right direction.
I decided to remove SwiftUI's memory management from the equation and tested with simple value and reference types:
private func doMemoryTest() {
struct ContentView {
let model: Model
func pressButton() {
model.action?()
}
}
struct ViewB {
let model: Model
func onAppear() {
model.action = action
// { [weak model] in
// model?.action = action
// }()
}
func onDisappear() {
print("on ViewB's disappear")
model.action = nil
}
private func action() {
print("Hey!")
}
}
class Model {
var action: (() -> Void)?
deinit {
print("*** DEALLOCATING MODEL")
}
}
var contentView: ContentView? = .init(model: Model())
var viewB: ViewB? = .init(model: contentView!.model)
contentView?.pressButton()
viewB?.onAppear()
contentView?.pressButton()
// viewB?.onDisappear()
print("Will remove ViewB's reference")
viewB = nil
print("Removed ViewB's reference")
contentView?.pressButton()
print("Will remove ContentView's reference")
contentView = nil
print("Removed ContentView's reference")
}
When I ran the code above, this was the console output (no deallocation of Model, as you observed):
Hey!
Will remove ViewB's reference
Removed ViewB's reference
Hey!
Will remove ContentView's reference
Removed ContentView's reference
In the above example it looks like I'm in complete control of the reference count on Model
, however when I inspected the memory graph in Xcode, I could confirm that Model
was retaining itself via action.context
(I'm not sure what that means):
To fix the retain cycle with minimal changes, you might want to consider removing Model's action assignment using ViewB.onDisappear
as I've done in my example. When I uncommented viewB?.onDisappear()
then I saw the following console output:
Hey!
on ViewB's disappear
Will remove ViewB's reference
Removed ViewB's reference
Will remove ContentView's reference
*** DEALLOCATING MODEL
Removed ContentView's reference
Good luck!
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.