Avoiding retain cycles in Swift
If you want to develop a high perfomance iOS app, sooner or later you will have to consider how your components are consuming the memory resources available and how you can optimize then.
One common problem related to memory management is the retain cycle problem. But before we define exactly what it is, how about seeing how iOS manages its own memory?
The automatic memory management method used by Apple is called ARC (Automatic Reference Counting). As its own name says, the reference count is used to determinate whether a memory block should or should not be deallocated.
When an object is created, its reference count begins with 1. This reference count can increase or decrease during its lifecycle. At last, when the reference count reaches 0, the object is deallocated from memory.
An example of this lifecycle can be seen below:
Strong and Weak References
Well, now is a good moment to explain what are strong and weak references. When declaring a variable we define if it is strong or weak. Variables are strong by default.
Strong variables increase the reference count. For instance, if an object has a reference count of 2 and is assigned to a new strong variable, its reference count increases to 3.
On the other hand, weak variables do not increase the reference count. If an object with a reference count of 2 is assigned to a new weak variable, its reference count will still be 2.
Furthermore, this means that, while a strong variable is active, its referenced component will be guaranteed to remain in memory. However, this guarantee will not apply to weak variables.
Retain cycles and memory leaks
You have probably seen a model in which an entity A has multiple instances of an entity B and each B entity is associated with an A entity. For example, a book can have many pages and every page belongs to a book.
In a very simplified implementation, these models could be something like this:
So far so good, uh? Well, not so much…
Can you see that the book has a strong reference to its pages and the page has a strong reference to the book? Guess what happens next. That’s right, a memory leak.
A memory leak occurs when a content remains in memory even after its lifecycle has ended.
In this case, the memory leak is caused by two strong variables that address each other. This problem is known as the retain cycle problem.
Ok, now let’s see some solutions.
Structs and Classes
A choice between a struct and a class can cause a retain cycle.
Both structs and classes can have constants, variables, functions and protocols. So, what is their difference?
The difference is that structs are passed by value and classes are passed by reference. Ok, but what does that mean?
When a variable is passed by value, its content is copied and assigned to another variable, keeping it intact.
Take a look at this code:
The output from this is:
Element(name: “A”, number: 1)
Element(name: “B”, number: 2)
Although we created a second variable from the first variable and changed its value, the value from the first one remained the same.
However, when a variable is passed by reference, the new variable will be a reference to the other one instead of a copy. So, when one of them changes, the other one will follow.
What happens if we use a class now?
Then the output will be:
Element(name: B, number: 2)
Element(name: B, number: 2
So, with the same steps, both variables will hold the same value and the original value will be lost.
Fine! But why we are talking about passing by value and passing by reference after all?
Well, remember when I told you that the choice between a struct and a class could cause retain cycles?
Let’s go back to the book and pages examples, but using a struct:
Now there won't be a retain cycle since the book assigned in the initializer will actually be an copy.
We can also keep using classes, but using a weak variable:
Just remember that now the book will be an optional and eventually it could become nil.
Protocols are widely used in Swift and can be adopted by classes, structs and enums. However, in some cases, if you are not careful enough, they can also cause retain cycles.
Imagine a generic delegate like this:
The delegate variable in this ListViewController is a strong one and can hold any type that conforms to its protocol. However, what if one this types is a class which has a strong reference to the view controller itself? Risky, right?
Changing the delegate variable to weak might do the trick, but if you try it you will get a compile error, since structs and enums cannot be assigned to weak variables.
But fear not, there is still a way! When we declare a protocol we can specify that only class types can adopt it. That way we can use weak variables as usual. Just look:
Closures are one of the most robust features available in Swift. Yet they are note retain cycle proof.
Closures can cause retain cycles for a single reason: by default, they have a strong reference to the object that uses them.
In this example we have a retain cycle with closures:
This object has a strong reference to the closure and, since we used self in the closure block, the closure has a strong reference to the object itself.
We can solve this in two ways.
First, we can use [unowned self]:
Now the closure doesn't have a strong reference anymore. Just be careful when using [unowned self] since that, if the object has already been deallocated when the closure is called, a crash will occur.
We can also [weak self]:
[weak self] achieves the same result as [unowned self] but is handled as an optional.
Great! If you got here, you already know the basics of handling retain cycles in Swift. Remember this tips when declaring variables, protocols and closures and you'll avoid many problems.
I am André. A young brazilian mobile developer that loves everything related to technology. If you have any doubt or suggestion, feel free to contact me here.