Back to blog

How To Fix Strong Reference Cycles in Swift


Aasif Khan
By Aasif Khan | December 22, 2021 7:09 pm  | 5-min read

A strong reference cycle causes a memory leak in your iOS app, and that’s a bad user experience. In this tutorial, we’ll discuss how you can resolve a strong reference cycle.

Here’s what we’ll get into:

  • What’s a strong reference cycle, and where do they come from?
  • Resolving reference cycles with properties and closures
  • Common pitfalls that result in a strong reference cycle
  • Working with weak in properties and closures
  • (And a lot of deliberately caused reference cycles for learning fun!)

How Memory Management Works on iOS

Before we dive into fixing strong reference cycles, let’s do a quick recap of memory management on iOS. In the previous 2 tutorials in this series, we’ve discussed:

  1. Automatic Reference Counting (ARC)
  2. Strong vs. Weak References

Memory management is needed in an app to determine what data can be removed from RAM memory, because that data isn’t needed any more. Memory is freed up to make space for other data. If you don’t free up space, you’ll eventually run out of space. Apps that don’t manage memory properly can be killed by iOS, which is a bad user experience.

On iOS, memory management happens through a mechanism called Automatic Reference Counting. ARC only affects reference types. With ARC, every instance (of a class) keeps track of who’s holding onto it. When the instance’s so-called retain count is zero, it’s deallocated and removed from memory. This mechanism is (almost) automatic.

References to instances are strong by default in Swift. A strong reference increases an instance’s retain count by 1. A weak reference, marked with the weak keyword, does not affect an instance’s retain count. You typically encounter weak/strong references when working with properties and closures.

When you manage memory poorly, you may accidentally create a strong reference cycle. This happens when instance A holds onto instance B, and vice versa. Both instances cannot be deallocated, because both their retain counts are greater than zero. This creates a memory leak. Cause enough memory leaks, and you run out of memory, which prompts iOS to kill your app.

In a weird way, all you need to do yourself when managing memory is avoiding strong reference cycles. It’s the end goal: avoid strong reference cycles! Everything else is automatic; done for you for free. To get there though, we’ll have to wade through some memory management concepts and principles. Let’s get to it!

What’s A Strong Reference Cycle?

A strong reference cycle happens when 2 instances hold strongly onto each other, and cannot be removed from memory as a result. This creates a memory leak; memory that cannot be deallocated, and wastes space.

Automatic Reference Counting (ARC) keeps track of which instances can be removed from memory, by their retain count. When the retain count reaches zero, the instance is deallocated. When a strong reference cycle happens, the cyclic strong references cause the retain count to remain greater than zero.

The retain count of instance A cannot be decreased to zero, because of the strong reference from instance B. And the retain count of instance B cannot be decreased to zero, because of the strong reference from instance A. Hence the “cyclic” nature of a strong reference cycle.

A Practical Example

Let’s take a look at an example! Check this out:

class Orchestra
{
let name:String
var conductor:Person?

init(name: String) {
self.name = name
print(“\(name) is initialized”)
}

deinit {
print(“\(name) is deinitialized”)
}
}

class Person
{
let name:String
var orchestra:Orchestra?

init(name: String) {
self.name = name
print(“\(name) is initialized”)
}

deinit {
print(“\(name) is deinitialized”)
}
}

In the above code, we’ve created two classes Orchestra and Person. Both classes have init() and deinit functions, that are used to initialize and deinitialize an instance of these classes, respectively. We’ll know when a Person or Orchestra class is created or deallocated, thanks to print().

You also see a few properties. Both classes have a name property, with either the name of the orchestra or the person. They’re printed out in the init and deinit functions.

The Orchestra class also has a property conductor of type Person?, and the Person class has a property orchestra of type Orchestra?. You can imagine that an orchestra has one conductor, and any person can belong to one orchestra. Note that these properties are strong by default, and they’re optionals.

Creating a Cyclic Reference

Let’s put these classes together and make something. Here’s what we got:

var airguitars:Orchestra? = Orchestra(name: “Dutch Intergalactic Airguitar Orchestra”)
var bob:Person? = Person(name: “Bob ‘Loud Ninja’ Applepie”)

So far so good! We’ve created instances of both Orchestra and Person, and assigned them to variables airguitars and bob respectively. Note that both variables are optionals, so we can set them to nil later.

Next, we’re going to deliberately create a strong reference cycle. Like this:

airguitars?.conductor = bob
bob?.orchestra = airguitars

What’s going on? We’ve assigned bob to the conductor property of airguitars, and assigned airguitars to the orchestra property of bob. You can see how this creates a 2-way link between the instances. Technically, we still have one Orchestra instance and one Person instance, it’s just that the variables and properties now hold references to them.

This makes sense – Bob’s the conductor of the Intergalactic Airguitar Orchestra, and that orchestra is Bob’s. In the real-world, you may want to know who the conductor of an orchestra is, or which orchestra a person is a member of.

Attempting to Deallocate

Back to the code. We’re going to add two more lines:

airguitars = nil
bob = nil

This explicitly sets both airguitars and bob to nil. As far these variables are concerned, they do not hold a reference to the Orchestra and Person instances any more.

The output of the code, however, is this:

Dutch Intergalactic Airguitar Orchestra is initialized
Bob ‘Loud Ninja’ Applepie is initialized

Hmm. You’d expect to see a is deinitialized line for both instances! They were set to nil, but why aren’t they deinitialized and deallocated?

Here’s what happened:

  • When we assigned the Orchestra and Person instance to airguitars and bob, we created a strong reference between the instance and the variable. This increased the instance’s retain counts by 1.
  • We also assigned bob to the conductor property, and airguitars to the orchestra property. A strong reference between the instance and the property. This increased their retain counts by 1 too, because both properties are strong.
  • At the end of the code, we’re setting airguitars to nil. This removes the reference between airguitars and the Orchestra instance, decreasing its retain count by 1. That strong reference is broken. The Orchestra instance itself isn’t deallocated, because it’s still assigned to the orchestra property of bob.
  • Finally, the bob = nil code is called. This breaks the strong reference between the bob variable and the Person instance. Like before, the Person instance itself is not deallocated because a strong reference from that Person instance to the conductor property of the Orchestra instance still exists. The Orchestra instance isn’t deallocated either, because of that reference with the orchestra property of Person.

Note: A local variable in a function is deallocated at the end of that function, unless you’ve passed the value on to somewhere else in your code. So when you do var elo = Orchestra() in a function, that elo variable is deallocated – implicitly set to nil – at the end of the function, when the function quits, unless you pass elo to another function, or a property, or a closure, etcetera. This is a good thing, of course, but you’ll have to be mindful of strong references.

A Strong Reference Cycle is Born

And there you have it! Both the Person and Orchestra instances cannot be deallocated, because they’re are holding onto each other in a cyclic fashion. The retain counts of the instances won’t decrease to zero, because their properties strongly hold on to the other instance.

A few questions spring to mind here. Why aren’t the instances deallocated when we set them to nil? Why won’t ARC recognize the strong reference cycle, and get rid of it altogether?

It’s important to understand the difference between a reference and an instance. In the code example, we only have 2 instances, but we have 4 strong references. Of these strong references, 2 are broken with airguitars = nil and bob = nil, while the other 2 remain in effect as properties.

Automatic Reference Counting (ARC) is able to deallocate instances based on their retain count. Strong references increase this retain count, as a way of saying: “I’m holding onto this instance, don’t remove it.” Your code naturally leads to the deallocation of (local) instances, for example, when a function reaches its last line, returns and quits.

When 2 instances hold on to each other in a cyclic fashion, there’s no way of knowing which reference to break. Breaking either one by force can lead to all sorts of problems in the app’s execution, so preventing strong reference cycles altogether is a better alternative.

Unlike Garbage Collection (GC), Automatic Reference Counting is baked into your app’s code. There’s no separate process to find and discard retain cycles or memory leaks. This means you either manage memory as part of your app’s code or risk creating memory leaks.

Fixing Strong Reference Cycles

How can you resolve a strong reference cycle? You’ve got 2 options here:

  1. Explicitly set a reference to nil
  2. Explicitly mark a strong reference as weak

1. Set a Reference to “nil”

Let’s check out setting to nil first, because it’s the simplest to work with. In this scenario, like the example code we’ve used thus far, you’ve got a strong reference cycle. Assume it’s not an option to make a reference weak, so you’ll have to break the cycle another way.

Here’s how you do that:

// Strong reference cycle is created
airguitars?.conductor = bob
bob?.orchestra = airguitars

// One of 2 references is broken
airguitars?.conductor = nil

// Deallocate explicitly
airguitars = nil
bob = nil

This results in the following output:

Dutch Intergalactic Airguitar Orchestra is initialized
Bob ‘Loud Ninja’ Applepie is initialized
Bob ‘Loud Ninja’ Applepie is deinitialized
Dutch Intergalactic Airguitar Orchestra is deinitialized

What’s going on here? The trick lies in setting airguitars?.conductor to nil explicitly. This decreases the retain count of the Person instance, which is then deallocated normally on the last line of the code.

You may not always have this opportunity in your code, but it’s a good place to start. When you suspect you’ve created a strong reference cycle, attempt to break the cycle by (hypothetically) setting one of the strong references to nil. You “simulate” deallocation in deinit, except it happens before the deinit is even attempted.

You may be tempted to set conductor to nil in deinit, wanting to break the cycle. This doesn’t work because deinit isn’t called at all! It’s good thinking though, because in some classes and frameworks you’ve got the opportunity to unset properties prior to deinit. For example, you can set (strong) properties to nil in the viewDidDisappear() function of a view controller.

2. Mark a Reference as “weak”

The more complicated alternative to fixing a strong reference cycle is marking a reference as weak. You can do this with both properties and closures.

Let’s take a look at what that looks like for our Orchestra and Person classes. In the previous section, we’ve discovered that the 2-way link between Orchestra and Person, through their orchestra and conductor properties, causes a strong reference cycle.

Here’s the offending code:

airguitars?.conductor = bob
bob?.orchestra = airguitars
We can mark either one of these properties as weak. Like this:

class Orchestra
{
let name:String
weak var conductor:Person?

}

In the above code, we’ve marked the conductor property with the weak keyword. This means that an instance assigned to the conductor property won’t have its retain count increased; the conductor property will not hold onto an instance that’s assigned to it.

As expected, this breaks the strong reference cycle. In the code’s output, after the above change, we can see that both Orchestra and Person are deallocated.

Dutch Intergalactic Airguitar Orchestra is initialized
Bob ‘Loud Ninja’ Applepie is initialized
Bob ‘Loud Ninja’ Applepie is deinitialized
Dutch Intergalactic Airguitar Orchestra is deinitialized

Why only mark one property as weak and not both? You only need to break the strong reference cycle in one place. Because conductor is now weak, when bob is set to nil, it’s deallocated because airguitars (of type Orchestra) does not hold onto the Person instance.

It’s important to note here that you can’t just blindly mark any property as weak – there are consequences. Consider the following code:

func createOrchestra(name: String)
{
let orchestra = Orchestra(name: name)
orchestra.conductor = Person(name: “Jane Doe”)

// Do something …
}

Looks innocent enough, right? This code hides a narly bug though. Because conductor is a weak property, the Person instance that’s assigned to it will immediately get allocated after assignment. The Orchestra instance doesn’t hold onto it – no one does – so as per ARC, the instance is removed from memory. Oops!

It’s a trivial example, but it underscores the reference counting principle of ARC. When you make a property weak, and an instance assigned to it isn’t retained anywhere else, that instance is deallocated.

The good news is that, theoretically, in the event of a strong reference cycle, there’s at least one other reference holding on to the instance. In practical iOS development, the above Person instance probably would be assigned to a property elsewhere too. You could even keep a local reference to it (i.e., a variable), and let that get deallocated at the end of the function.

Common Pitfalls in Practical iOS Development

Now that we’ve discussed all these principles and concepts, where exactly do strong reference cycles happen in day-to-day iOS development? Let’s discuss 2 common scenarios.

  1. A subview and parent view controller reference cycle
  2. A closure keeping a reference to self

Parent/Child Reference Cycle

This kind of strong reference cycle is a riff on the orchestra-conductor conundrum we resolved earlier. It’s a bit more insidious though, and a signal of a poorly thought out app architecture.

Here’s how it works:

  1. You’ve got a view controller, like DetailViewController
  2. This view controller has a custom subview, as a property: var topView = CustomView()
  3. Inside the custom subview, you need to access some data that’s available within the view controller
  4. You create a property for that on the custom subview: var detailViewController:DetailViewController?
  5. After you initialize the custom subview, you assign self to that property, like this: topView.detailViewController = self
  6. You can now get to the view controller from within the subview: detailViewController.doSomething(). Neat! Except…

You’ve got a strong reference cycle between the view controller and the subview. The view controller is strongly referenced by the topView.detailViewController property, and the subview is strongly referenced by the topView property of DetailViewController. That’s a strong reference cycle right there!

How do you resolve it? It’s simplest to mark the detailViewController as weak. This breaks the strong reference cycle. In fact, this is exactly what the delegation pattern on iOS uses. Delegate properties, like dataSource for a table view controller, are weak by design to avoid reference cycles.

Note: In general, it’s smart to avoid “linking back” from a subview to a “parent” view controller. It helps you prevent tightly-coupled code. Instead, you can formalize the relationship between view controller and subview through delegation and a protocol. You can also provide the subview the information it needs upon initializing it, and find another approach to pass data back to the view controller.

A Closure With “self” Reference Cycle

Another common strong reference cycle pitfall happens with closures. Closures are self-contained blocks of code that you can pass around, kinda like functions assigned to variables.

Closures can “capture” values from their surrounding scope, which means you can use variables from outside the closure inside it – even when that closure is executed at a later point in your code. Neat!

When a closure captures an instance (i.e., reference type) from its surrounding scope, it’ll create a strong reference to that instance. And you’ve guessed it: that could potentially create a strong reference cycle!

Check this out:

class DetailViewController: UIViewController
{

func viewDidLoad()
{
super.viewDidLoad()

let task = URLSession.shared.dataTask(with: “https://imgur.com/cats.png”, completionHandler: { data, response, error in

self.doSomething(withData: data)
})
}

func doSomething(withData data: Data) {

}
}

What’s going on here? In the above code, we’ve got a simple view controller that executes an async HTTPS request with URLSession for a certain cats.png image file. When the image has been downloaded, its data is passed to a doSomething(withData:) function of the view controller.

Imagine that this view controller is part of a navigation controller. At some point, the user uses the Back button and navigates away from the view controller. This happens while the image download task is still running in the background – it’s a big cat image.

The closure has now “outlived” the view controller it was declared in. Because we’re calling the function doSomething() inside the closure, the closure has created a strong reference with self. This is also made explicit with the self. code inside the closure.

Assuming that the closure itself is strongly referenced in URLSession, there’s now a strong reference cycle between the closure and the view controller instance. This should technically resolve itself once the closure is executed and subsequently set to nil, unless the closure is stored as a strong property somewhere.

You can avoid a strong reference cycle here, like this:

let task = dataTask(with: “https://imgur.com/cats.png”, completionHandler: { [weak self] data, response, error in

self?.doSomething(withData: data)
})

Thanks to the weak keyword in the [weak self] capture list, the reference to self is made weak. As a result, the closure doesn’t keep a strong reference to the view controller (i.e., “self”) and the retain cycle is broken. Of course, the URLSession could make the property the closure is stored in weak, but that may not be an option you have control over.

You can also use [unowned self] instead of [weak self], or if you’re using a property, use something like [weak imageView]. In short, unowned will not result in an optional, but you need to ensure that you’re not using the reference if it’s nil. You typically use unowned when the closure does not outlive the referenced/captured instance.

Further Reading

Pfew! We’ve come a long way. In this tutorial, we’ve taken a closer look at strong reference cycles and how to resolve them.

A strong reference cycle happens when 2 instances keep a strong reference to each other. You can accidentally create such a cyclic reference, for example when working with 2-way “links” between objects, or with closures. You can break the cycle by marking a reference as weak, or by setting one of the references to nil. Neat!


Aasif Khan

Head of SEO at Appy Pie

App Builder

Most Popular Posts