Go to previous page

Understanding Retain Cycles in Swift's ARC: A Simple Guide

Memory Leak
#
Swift
#
iOS
January 29, 2024

Memory leaks are every developer's worst nightmare. These silent bugs occur when a program doesn't properly release allocated memory, causing a gradual buildup that can slow down or crash the system. At first you don’t even know they can exist, then you ignore them, and then you start seeing them everywhere without knowing how to handle them properly. Finding and fixing memory leaks is like navigating a maze of code, requiring careful scrutiny, debugging tools, and rigorous testing.

Through this article, let’s address this problem once and for all, and also dive into how elegantly Swift handles memory leaks.

Apple has made an insightful article about strong reference cycles, which you can check out here. It’s easy to understand what a memory leak is, and how to avoid them in the case of class instances. However, these are rare scenarios and often easy to spot and fix. Personally, I find memory management in closures a lot more confusing and difficult to understand. Here, we will first quickly go through retain cycles in class instances, and then have a thorough look at closures.

Pre-requisites

Incase you are completely unfamiliar with the concept of ARC, here is a quick description from the official Apple documentation:

Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you don’t need to think about memory management yourself. ARC automatically frees up the memory used by class instances when those instances are no longer needed.

This means that every time a class instance is created, ARC allocates a certain chunk of memory to that particular instance. It also keeps a track of the number of strong references pointing TO that instance. Strong reference is a way of keeping an object alive in the memory, so that it is not deallocated while it’s still being used by the program. Whenever an object is created and assigned to a variable, it creates a strong reference pointing TO the object. Let’s take an example to understand this.

	
Copy
class Car { let model: String init(model: String) { self.model = model print("\(model) car is being initialized") } deinit { print("\(model) car is being deinitialized") } } var myCar: Car? // Creating a strong reference myCar = Car(model: "Tesla") // Creating an additional strong reference var friendCar: Car? = myCar // Breaking one strong reference myCar = nil // Note that the `deinit` method is not called here // Breaking the second strong reference friendCar = nil // `deinit` method called here since all strong references are broken

Retain Cycles in Class Instances

Now, although Swift automatically keeps a track of number of strong references pointing to an object, there might be a situation wherein a class instance never gets to a point where it has zero strong references pointing to it. Let’s understand this using an example.

This example defines two classes called <span class="text-color-code"> Person </span> and <span class="text-color-code"> Car </span> , which model cars and their owners.

	
Copy
class Person { let name: String init(name: String) { self.name = name } var car: Car? deinit { print("\(name) is being deinitialized") } } class Car { let model: String init(model: String) { self.unit = unit } var owner: Person? deinit { print("Car \(model) is being deinitialized") } } var john: Person? var tesla: Car? john = Person(name: "John") tesla = Car(model: "Tesla") john?.car = tesla tesla?.owner = john

Here’s how the two instances look in memory right now.

Now, if we break the strong references between the variables <span class="text-color-code"> john </span> and <span class="text-color-code"> tesla </span> and their respective instances, there would still exist strong references pointing to each object by the other, hence keeping both of them alive in the memory. This is called a retain cycle and will cause a memory leak.

Now, let us try to briefly understand how a similar retain cycle may be created in closures.


Retain Cycles in Closures

To understand the formation of retain cycles in closures, we first need to understand what a closure is and how it works. By definition, closures are self-contained blocks of functionality that can capture and store references to variables and constants from the surrounding context. Since this definition is a little tricky to understand, I like to picture it as a piece of code, which when declared, creates its own temporary class that contains a reference to all the objects it needs in order to execute itself.

Let’s take a simple example to understand it. Consider the following code:

	
Copy
class CustomView: UIView { var onTap: (() -> Void)? // ... } class ViewController: UIViewController { let customView = CustomView() var buttonClicked = false func setupCustomView() { var timesTapped = 0 // Assigning value to the closure customView.onTap = { guard let self = self else { return } timesTapped += 1 print("button tapped \(timesTapped) times") self.buttonClicked = true } } }

Here, we have a  ViewController that has a CustomView. That CustomView has a closure that is called when a button is tapped.

When we give a value to the closure, it needs to keep a reference to some variables in order to execute itself. Here, it needs <span class="text-color-code"> self </span> and <span class="text-color-code"> timesTapped </span>.

Why <span class="text-color-code"> self </span>, you ask? Since the closure is declared within the CustomView class, the instance of which is created in the ViewController, it is a primary requirement that the instance of the ViewController class (<span class="text-color-code"> self </span>) is present in the memory. Hence, the closure keeps a strong hold on it. This will prevent those values from being freed, and the closure from crashing in case they were deallocated.

But wait, we are missing out on something. Note that the ViewController has a strong reference to CustomView, that has a strong reference to the closure, which just created a strong reference to self. Do you see the cycle yet? Here is a visual representation of what is happening in this scenario:

As you can see clearly, we have a cycle, meaning that even if we exit the view controller, it can’t be removed from the memory because it’s still referenced by the closure. This is again a retain cycle that will cause a memory leak.

Solution

Now that we’re familiar with how retain cycles are caused, both with class instances and closures, let us see how to tackle them. Swift provides two ways to resolve strong reference cycles when we work with properties of class type: weak references and unowned references.

Weak and unowned references enable one instance in a reference cycle to refer to the other instance without keeping a strong hold on it. The instances can then refer to each other without creating a strong reference cycle.

It is essentially the programmer telling Swift which reference is important enough that it decides whether the instance should be kept in memory and which one isn’t. In the case of weak or unowned references, Swift can easily deallocate the objects since there are no strong references pointing to them.

Weak and Unowned references can be defined by using the keywords <span class="text-color-code"> weak </span> or <span class="text-color-code"> unowned </span> before variable declarations.

What’s the difference between the two?

Use a weak reference when the other instance has a shorter lifetime — that is, when the other instance can be deallocated first. In the Car example above, it’s appropriate for a car to be able to have no owner at some point in its lifetime, and so a weak reference is an appropriate way to break the reference cycle in this case. In contrast, use an unowned reference when the other instance has the same lifetime or a longer lifetime.

⚠️ IMPORTANT
Because a weak reference doesn’t keep a strong hold on the instance it refers to, it’s possible for that instance to be deallocated while the weak reference is still referring to it. Therefore, ARC automatically sets a weak reference to nil when the instance that it refers to is deallocated. And, because weak references need to allow their value to be changed to <span class="text-color-code"> nil </span> at runtime, they’re always declared as variables, rather than constants, of an optional type.However, unlike a weak reference, an unowned reference is expected to always have a value. As a result, marking a value as unowned doesn’t make it optional, and ARC never sets an unowned reference’s value to <span class="text-color-code"> nil </span>.

Let us modify our first example to see how creating a weak reference avoids a retain cycle:

	
Copy
class Person { let name: String init(name: String) { self.name = name } var car: Car? deinit { print("\(name) is being deinitialized") } } class Car { let model: String init(model: String) { self.unit = unit } weak var owner: Person? deinit { print("Car \(model) is being deinitialized") } } var john: Person? var tesla: Car? john = Person(name: "John") tesla = Car(model: "Tesla") john?.car = tesla tesla?.owner = john

The only change we made here is adding the <span class="text-color-code"> weak </span> keyword before the <span class="text-color-code"> owner </span> property of the Car. Let us now try to visualise how it looks in the memory:

Notice that now, if we try to make the <span class="text-color-code"> john </span> variable <span class="text-color-code"> nil </span>, there would be no strong references left pointing TO the Person instance. Hence, the strong reference count drops to 0 and the instance is immediately deallocated. Since this instance is now deallocated (does not exist), there is only ONE strong reference pointing to the Car instance, by the variable <span class="text-color-code"> tesla </span>. If we make <span class="text-color-code"> tesla </span> also <span class="text-color-code"> nil </span>, the car instance is also deallocated.

	
Copy
john = nil // Prints "John is being deinitialized" tesla = nil // Prints "Car Tesla is being deinitialized"

Similarly, in the case of closures, to break a cycle, we just need to break one reference, and the easiest one to break is the last link, when the closure references itself. To do so, we need to specify when capturing a variable that we don’t want a strong link. The two options that we have are: weak or unowned and we declare it at the very beginning of the closure. Like this:

	
Copy
cell.onButtonTap = { [unowned self] in self.navigationController?.pushViewController(NewViewController(), animated: true) }

Now, since the closure does not have a strong hold on the object it captures, ARC can deallocate it appropriately.

The same rule applies for closures also. Use <span class="text-color-code"> unowned </span> when the closure cannot exist longer than the object it captures, and <span class="text-color-code"> weak </span> otherwise.

For better understanding of <span class="text-color-code"> weak </span> and <span class="text-color-code"> unowned </span> references and when to use them, I would highly recommend checking out this article. Also, the official Swift documentation has detailed examples of when to use either, which you can checkout here.

Conclusion

In conclusion, understanding Automatic Reference Counting (ARC) and recognising the pitfalls of retain cycles is crucial for maintaining a healthy and efficient iOS application. By employing capture lists with weak or unowned references, developers can navigate the intricate landscape of memory management. I hope this exploration into ARC and retain cycles has been helpful, providing clarity and dispelling any doubts you may have had. Happy coding!

These are some references I utilised for my research, and you may find them beneficial:

Recommended Posts

Open new blog
Maybelline and Microsoft teams virtual try-on technology
#
beauty-tech
#
collaboration
#
innovation

Microsoft Teams up with Maybelline’s AI 'Makeup' Filters

Sohini
August 17, 2023
Open new blog
IoT
#
IoT
#
product

IoT Product Development: 10 essential steps to success

Sohini
September 8, 2023
Open new blog
android 14
#
Android
#
technology

14 Changes in Android 14: Vital Insights That Android Developers Need to Know

Harsh
October 30, 2023