Skip to Content
Course content

149: Memory Graph Debugging

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I’ve seen a lot of developers hit a wall when they notice their deinit methods aren't firing. Their immediate instinct is usually to go back to the source code and search for every instance of a strong reference, hoping to spot the cycle by eye. They assume that if they just look hard enough at the strong and weak keywords in their files, the leak will reveal itself.

Searching for "strong" in the code is enough to find leaks

The problem with this approach is that in Swift, everything is strong by default. You aren't writing the word strong next to every property; you're just writing var coordinator: Coordinator. When you have a complex app with objects passing references back and forth, the "cycle" isn't a single line of code—it's a path. It might go from ViewController to ViewModel, then to a NetworkService, then to a closure, and finally back to the ViewController.

Trying to trace that path mentally is a nightmare. I've spent hours doing this, only to realize the leak was caused by a closure I forgot to use [weak self] in, but the closure was wrapped inside a service I didn't even think was part of the loop. You can't "grep" your way out of a runtime memory leak.

Visualizing the runtime graph to find the hidden culprits

This is where the Memory Graph Debugger becomes your best friend. Instead of guessing, it lets you pause the app and see exactly who is holding onto whom at that precise moment in time. When you click the "Debug Memory Graph" button (those three connected circles in the debug bar), Xcode transforms your heap into a visual map.

Here is how I actually use it to kill leaks. Let's say we have a ChatSession that manages a User, and the User has a callback closure for when their status changes. If that closure captures the ChatSession strongly, neither will ever be freed.

class ChatSession {
    let user = User()
    deinit { print("Session freed") }
}

class User {
    var onStatusChange: (() -> Void)?
}

// The leak happens here
let session = ChatSession()
session.user.onStatusChange = {
    print("Session \(session) updated!") 
}
// Even if we set session = nil, it stays in memory.

In the Memory Graph Debugger, you won't just see a list of objects. You'll see the ChatSession instance with a bold arrow pointing to the User, and a bold arrow pointing from the User's closure back to the ChatSession. Xcode often flags these automatically with a purple exclamation mark, explicitly telling you "Cycle Found."

The real power is in the Navigator on the left. You can select the leaked ChatSession and see every single reference pointing to it. If you see a reference coming from a malloc block or a Swift closure context, you know exactly where to look in your code: find the closures that interact with that object. I always look for the "thick" lines first—those represent strong references. If you see a loop of thick lines, you've found your leak.




📋 Practical Task

Exercise: Hunting the Playlist Retention Cycle

You are working on a music app. The MusicPlayer owns a Playlist, and the Playlist has an onTrackEnded closure that updates the MusicPlayer's UI. However, the MusicPlayer is never being deallocated when the user closes the player view.

Your Task:

  1. Create the following classes in a Swift Playground or Xcode project:
    • MusicPlayer: Has a property var currentPlaylist: Playlist? and a deinit that prints "Player Deallocated".
    • Playlist: Has a property var onTrackEnded: (() -> Void)?.
  2. In your setup code, instantiate a MusicPlayer, create a Playlist, and assign it to the player.
  3. Assign a closure to playlist.onTrackEnded that references self (the player) to update a property.
  4. Set the player reference to nil and observe that "Player Deallocated" is not printed.
  5. Run the Memory Graph Debugger. Locate the MusicPlayer instance and trace the reference path to find the closure causing the cycle.
  6. Fix the leak using a capture list and verify that the deinit message now prints.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.