Retain Cycles in Swift

Publish date: March 21, 2021
Tags: swift, ios

Once you get past your initial introduction to Swift and iOS development and start working on a bigger project - especially if you don’t already have a background in programming - you will probably hit your first intermediate level problem: Memory management and retain cycles.

What causes retain cycles?

To answer this, it is required to understand how Automatic Reference Counting (ARC) works. It can be summarized with four simple sentences:

Any reference that doesn’t have the weak or unowned keyword is a strong reference.

To better understand this, I read a great analogy some years ago:


If you have two or more objects, that hold references "in a circle", it will cause a retain cycle.

Going back to the dog analogy before, if you have three dogs tied to one another with a leash, neither dog will be able to run away.

Or in code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class BlackDog {
    var otherDog: BrownDog?
}

class BrownDog {
    var otherDog: GrayDog?
}

class GrayDog {
    var otherDog: BlackDog?
}

func createCycle() {
    let blackDog = BlackDog()

    let brownDog = BrownDog()
    blackDog.otherDog = brownDog

    let grayDog = GrayDog()
    brownDog.otherDog = grayDog

    // This is the step that seals the cycle
    grayDog.otherDog = blackDog
}

NOTE: You can think of closures also as objects.

To break the cycles, you need to replace any one of the references with weak or unowned. I plan to cover the difference between the two in my next article. For now, I would recommend using weak.

For example:

1
2
3
class GrayDog {
    weak var otherDog: BlackDog?
}

How to debug retain cycles?

The simplest and fastest way I’ve found to check if my project has retain cycles is by using Xcode’s built in Memory Debugger.

Screenshot showing Xcode’s Memory Debugger

  1. Run your app and look at the memory usage
  2. Navigate through your app, trying to get to each possible screen
  3. Close every screen and go back to the starting point
  4. With the Simulator in focus, hit ⌘⇧M (Cmd + Shift + M), or Debug -> Simulate Memory Warning;

This is important in case you are using any sort of caching library (ex: Kingfisher), because they usually have an in-memory cache as well.

  1. Check the memory graph and compare it with the value from step 1.
  2. If there is a significant discrepancy, open up the Memory Graph and check for any suspicious items

The fact that there is a difference in memory does not necessarily indicate that there are retain cycles. It could be that some objects (usually Singletons) got allocated later than app start. I recommend repeating steps 2. and 3. to see if there’s an increase in memory every time.

Screenshot showing Xcode’s Memory Graph

You can use the button highlighted with yellow to hide Apple’s frameworks and only see objects that are in your workspace.

I usually start by looking at ViewControllers, then components of my architecture (ex: ViewModel, Coodinator) and finally Services which are not Singletons.

Conclusion

Hopefully now you have a better understanding of what causes retain cycles and how to debug them.

As always, I would love to hear your thoughts and suggestions. Feel free to get in touch at moc.htikmsofi@gomlb!