Skip to Content
Course content

151: Diagnosing Retain Cycles with Memory Graph

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

Imagine two people, Alex and Sam, standing in a room. They’ve decided to hold onto each other's wrists in a firm, locked grip. Now, imagine the rule of the room is that you can only leave once you aren't being held by anyone. Alex won't let go until Sam does, and Sam won't let go until Alex does. They're stuck. Even if the rest of the party has gone home and the lights are turned off, Alex and Sam are still there, occupying space, simply because they refuse to be the first one to let go.

In Swift, this is exactly what happens during a retain cycle. Your objects are Alex and Sam, and the "grip" is a strong reference. ARC (Automatic Reference Counting) is the room manager; it sees that both objects still have a reference count of at least one, so it assumes they're still needed. It can't "clean the room" because neither object is truly free.

When Code Becomes a Deadlock

You've likely encountered this if you've used delegates or closures. Let's look at a common scenario: a ChatSession that needs to notify a ChatViewController when a message arrives. If the controller owns the session, and the session holds a strong reference back to the controller as its delegate, you've just built a deadlock.

class ChatSession {
    var delegate: ChatViewController? // Strong reference by default!
    
    func receiveMessage() {
        delegate?.didReceiveMessage("Hello!")
    }
}

class ChatViewController {
    var session: ChatSession?
    
    func setupChat() {
        session = ChatSession()
        session?.delegate = self
    }
}

At first glance, this looks fine. But the moment you dismiss that ChatViewController, it doesn't actually leave memory. The ChatSession is still holding onto it, and the controller is still holding onto the session. They're just standing there in the dark, wasting RAM.

Visualizing the Mess with the Memory Graph

You could hunt these down using deinit prints, but that's like trying to find a leak in a pipe by listening for drips. It's tedious. Instead, I want you to use the Memory Graph Debugger. It's a lifesaver when your app's memory usage starts climbing for no apparent reason.

While your app is running in Xcode, look at the debug bar (just above the console). There's a small icon that looks like three connected circles. Click that. Xcode will pause your app and generate a visual map of every object currently living in memory and who is holding onto whom.

Here is what I look for when I'm diagnosing a leak:

  • The Bold Lines: In the graph, bold lines usually represent strong references. If you see a path of bold lines that forms a circle (A → B → A), you've found your culprit.
  • The Leaked Icon: Xcode is often smart enough to flag suspected leaks with a purple exclamation mark. If you see one of those, start your investigation there.
  • The Navigator: On the left sidebar, you can filter by class name. I usually search for my ViewControllers. If I see five instances of ChatViewController in the list, but I've only ever opened one chat window, I know I have a retain cycle.

Breaking the Grip

Once the Memory Graph shows you the cycle, the fix is almost always the same: someone has to be willing to let go. In our analogy, either Alex or Sam needs to loosen their grip. In Swift, we do this by marking the reference as weak.

By changing var delegate: ChatViewController? to weak var delegate: ChatViewController?, you're telling ARC: "I want to know about this object, but I don't want to be the reason it stays alive." The moment the ChatViewController is dismissed, the strong reference from the owner is gone, the count hits zero, and the whole cycle collapses and clears out of memory. It's a clean exit.




📋 Practical Task

Debugging the UserProfileManager Leak

You have been handed a small project where the UserProfileManager is causing a massive memory leak. The UserProfileViewController creates a manager, and the manager stores a closure to handle profile updates. However, the closure captures self strongly, creating a retain cycle.

Your Goal:

  1. Run the project and navigate to the Profile screen, then go back to the Home screen three times.
  2. Open the Memory Graph Debugger and verify that three instances of UserProfileViewController are still residing in memory.
  3. Identify the strong reference cycle between the UserProfileViewController and the UserProfileManager's update closure.
  4. Fix the leak using a capture list ([weak self]) and verify in the Memory Graph that the controllers are now being properly deallocated when you leave the screen.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.