Retain Cycles and Memory Management in Swift

Build performant iOS applications

İsmail GÖK
Better Programming

--

Photo by Kirill Sharkovski on Unsplash

In this article, I would like to explain what the concepts of retain cycle and memory management are, and how you can improve your iOS app by simply knowing these concepts.

In Swift or any other OOP language, every instance is either a value type or a reference type.

Value type instance means whenever you want to create a variable from that instance and assign that variable to another one, it follows a concept called pass by value which means that it copies the value of the variable to another.

The change on one of them wouldn’t affect the other one because the original value was copied to the other instance and the copied value is still the same.

On the other hand, a reference type instance follows the concept of pass by reference which means that the value would not be copied but both of the variables would point to the same memory space. If one changes, the other one also changes because the values in that memory space would change.

Let me throw in a simple example to make it all clearer. Here is a value type example:

Since structs are value types, when we create a variable using a struct and assign it to another variable, Swift only copies the value of the first variable to the second one. If we want to change a property in the first variable, and print both of them as above, we can see that the first print statement prints out Audi and the second print statement still print out Mercedes.

Here is a Class example for the purpose of visualizing reference types:

First we create a variable out of Car class and assign it to another variable. Since classes are reference types, both variables (car1 and car2) points to the same memory address. When we change the brand property of car1 variable to Audi and print both variables brand properties, both print statements print out Audi.

Once we make everything clear on value and reference types which are fundamental concepts to understand memory management, we can go on to the real stuff.

Swift compiler deals with the memory management of value types (structs and enums).

In most of the scenarios, the compiler also deals with the memory management of classes which are reference types. However, you should be aware of some cases that classes cause memory problems that the compiler could not handle itself.

Normally when a reference type — for example “a class” — is done being used, the compiler would deallocate the instance, hence its space would be freed from the memory.

Reminder: In OOP, we could assign class variables to other class variables. That assignment is called pass by reference. It means that the reference of the instance would be copied to another variable, not the value of it.

This concept can cause some memory problems because more than one instance points to the same memory space, therefore the compiler itself cannot resolve when to deallocate the instances from the memory. In this scenario, Swift uses a concept called Automatic Reference Counting (ARC) to make the compiler’s task easier.

Automatic Reference Counting (ARC)

The idea behind the automatic reference counting is simple. Swift keeps track of the references created in the code by counting the references each class instance have in order to know when it is safe to deallocate the class instance from the memory.

ARC makes sure that a class instance isn’t deallocated as long as a property or variable has a strong reference to it. I will explain it on an example.

First let’s assume that John wants to buy a new car. He creates that object for the car using the class above. Whenever a new car object is created, the init function assigns the brand name and prints it.

When John wants to sell his car, the object would be deallocated from the memory, therefore the deinit function would be triggered.

Now, in order to demonstrate ARC in a practical example, we will create some instances from the MyNewCar object above.

We created three variables of type MyNewCar and did not initialized them. First we initialize one of the variables with our MyNewCar class constructor.

This operation prints Porsche is being initialized statement because a memory space in allocated for the reference of the class object hence init method will be invoked when we first initialize the object.

After that, we assign the porsche variable to the other two variables as above. I will explain what happens in the background of this assignment in the following paragraphs.

Then, we assign the porsche and mercedes variables to nil. Again, I will explain what happens in the background of this assignment in the following paragraphs.

Lastly, we set the last instance of that class (which is ford) to nil.

Only after when all instances of the same object is removed, the object is truly deinitialized and it is deallocated from the memory. Hence it prints out the statement in the deinit function.

Now, what exactly happened in this example? Did we create an object and copy it to the two other objects? This question is actually answered in the second paragraph of this article.

Classes are reference types and they are stored in the heap parts of the memory. When you assign two or more instances to the same class object, the content of the object wouldn’t be copied, they only point on the same address on the memory. So, no to copying values, only addresses from the memory object is contained.

Main concern of the example was to explain the concept of Automatic Reference Counting (ARC). ARC count the number of strong references to an object. When an instance is created and initialized from a class ARC is 1 for that object. When that instance is assigned to two other instances, ARC would be 3 (after assigning porsche to two other objects).

When we set the two of the instances to nil, we remove their connection to the memory address that contains the object. They don’t point to that address anymore. ARC would be 3–2 = 1 for that object again. Since ARC is still more than zero, that class object wouldn’t be deallocated and would still live in the memory.

Only when we set the last object that points to the object (which is the ford object), the ARC would be zero and deinit function of the class would be invoked and the reference of the class would be deallocated from the memory.

Strong vs Weak vs Unowned

What is strong reference? We will briefly look on what makes a reference strong or weak.

I will only briefly mention the concepts of strong, weak and unowned references in this article. For further explanation, I will be writing another article only on this concept.

In Swift, a variable can be declared as strong, weak or unowned in terms of reference it makes.

In order to make a variable strong, you don’t need to write anything in front of the declaration. Strong references protects the object which points to, from getting deallocated by ARC. As long as an object has a strong reference, it cannot be deallocated from the memory even if the screen containing the object has popped from the navigation stack.

For example, every variable in the examples above is making strong references since there are no keywords like weak or unowned.

Image by author

In contrast to strong references, a weak reference does not protect referred object from getting deallocated from memory when there is no strong reference points to it.

Weak references also does not increase the ARC count when pointing to an object. When there is no strong reference pointing on an object even if there are weak references pointing, ARC would be zero and it would be deallocated from memory making the weak references variable nil, hence all weak referenced variables must be optionals. In order to make a weak reference, we need to declare the variable as a var and put a weak keyword in front of it.

As mentioned above, weak referenced variable will be nil when there is no other strong reference left pointing on the object, therefore it must be a mutating variable. That means it cannot be a constant (it cannot be declared as a “let”).

An unowned reference is very similar to a weak reference except some subtle differences. Both unowned references and weak references does not increase the ARC when pointing on an object.

However, an unowned referenced object does not have to be an optional like weak referenced object does in Swift and they only should be used when you are certain that the reference will never be nil once it has been set during initialization.

This means that, unowned referenced object does not zero out when the object is deallocated from memory when there is no strong references on that object. This creates a concept called dangling pointer which every Swift developer should be aware of.

Therefore, unowned referenced object should be used like implicitly unwrapped optionals. I won’t be explaining the concepts of dangling pointer and implicitly unwrapped optionals since their details are not must-knows for this memory management article. I will be explaining them in details on an another article.

I assume that all is clear with the Automatic Reference Counting terminology. The next big thing is what happens if we do not pay attention to this ARC concept and how it hurts the performance of our application.

Retain Cycles and Memory Leaks

Image by author

We all know — at least now :) — in order to deallocate an object from memory, its ARC value must be zero. However, when some two object refers each other via strong references, they prevent the compiler from deallocating either object because their ARC value would always be 1. That is called a retain cycle.

Memory leak occurs when an amount of allocated memory space cannot be deallocated for some reason in iOS applications, the usage of memory for that application always grows while the app is running. Memory leaks may be very critical since they can use up all the memory reserved for the application quickly and making the iOS operating system force shut down the application.

Retain cycles often cause memory leaks since all of the objects that caused retain cycles cannot be deallocated from memory. I will give a small example for demonstrating how we can cause a retain cycle and an app with potential memory leaks.

Firstly, let’s say that John wants to buy a new car once again. In order to help him, we write a Car class like above. The class has two properties; one is brand which has the type of String, and the other one is driver which has the type of Person.

In parallel, we write a Person class that has two properties; name and car. Both classes have initializer and deinitializer functions for creating and destroying the objects that derived from these classes. When either of the objects are destroyed (or set to nil) deinit function should be invoked and print a responding statement.

After writing the classes, we initialize both of them using their initializers. Now, we need to set a driver to the Car instance (mercedes), and set a car to the Person instance (john). We do exactly that in line 5 and 6. That makes ARC values for both Car and Person object instances (which are mercedes and john) to 1.

When our job is done with them and we want to free their resource from memory, we want to destroy both of the objects like in line 8 and 9. We expect their ARC values should decrease to 0 and compiler to free their resource from memory. However, we don’t see any print statements from deinit functions of both classes. What went wrong is, we caused a retain cycle.

Since there are two strong references that refers to each other, Swift compiler cannot free their resources from the memory hence cannot decrease their ARC values when we set them to nil. For example; when we set mercedes instance to nil, the compiler cannot decrease its ARC to 1 because a Car object is still referencing the mercedes and vice versa.

In order to prevent this retain cycle, we need to declare at least one of the variable as weak or unowned. We can break the retain cycle with adding a weak keyword before either the driver property of the Car class or the car property of the Person class.

That was a short introduction to the retain cycles and memory leaks in order to understand the importance of the memory management concept in Swift. I will be adding more blogs about memory management and similar topics since it is a crucial concept to learn about in my opinion.

I hope this article can be a help for understanding the fundamental parts of memory management concepts running mostly behind the scene and help you implement high performance iOS applications. Stay healthy.

--

--