Find the leak behind memory that grows on every screen push and pop
Every time the user pushes DetailViewController and pops it, the app's memory climbs and never comes back down. You capture the Memory Graph Debugger after popping the screen three times and see three live instances:
DetailViewController 0x600001a (3 living instances)
|- strong --> closure (onData) 0x600002b
| |- strong --> DetailViewController 0x600001a
|- strong --> UITableView 0x600003c
Root set: (nothing holds DetailViewController)
Explain why the instances stay alive and give the one-line fix.
The graph shows the popped controller still alive: a strong arrow to a stored closure and a strong arrow back form a cycle, so it is never freed. Capture [weak self] and the controller deallocates on pop, ending the growth.
- ✗Blaming steady growth on caches instead of checking the retain graph
- ✗Expecting
viewDidDisappearor a manual nil to release a cycled controller - ✗Marking every edge
unownedinstead of breaking one withweak
- →How do you tell a strong arrow from a
weakone in the graph? - →Why does the leaked controller often bring an entire view hierarchy with it?
The graph's arrows are strong and they form a loop: the controller strongly holds the onData closure, and the closure captured self strongly and holds the controller back. Neither count reaches zero, so every pop leaves one more live instance. Break the cycle by capturing [weak self]:
final class DetailViewController: UIViewController {
var onData: ((Data) -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onData = { [weak self] data in // was: a strong self capture
self?.apply(data)
}
}
func apply(_ data: Data) { /* ... */ }
}
Now the closure no longer retains the controller, the count reaches zero on pop, deinit runs, and the memory growth stops.