Memory management in Swift: iOS

This article explains how ARC works, addresses potential memory leaks due to retain cycles, and an overview about different memory reference types in Swift.

Rohit Kumar
9 min readSep 27, 2024

Efficient memory management is essential for ensuring the smooth performance of applications, especially on mobile devices with limited resources. Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage.

ARC ensures that objects are kept in memory as long as they’re needed and are deallocated when they’re no longer in use. Understanding how ARC works and how to avoid memory management pitfalls is key to writing efficient Swift code.

What is ARC (Automatic Reference Counting)?

ARC is Swift’s memory management mechanism that automatically keeps track of the reference counts for class instances. When the reference count of an instance reaches zero, ARC deallocates it, freeing up memory. This process occurs automatically at runtime, which means developers do not need to manually allocate or deallocate memory.

How ARC Works

ARC works by maintaining a strong reference count for every instance of a class. Each time you assign a class instance to a variable or constant, or pass it to a function, the reference count increases. When the reference is no longer needed, the count decreases. If the reference count reaches zero, ARC frees the memory allocated to that instance.

In Swift, types are divided into reference and value types based on how memory is managed.

Reference Types: Classes are reference types, meaning multiple variables can reference the same instance. Changes to one reference affect all, and ARC tracks the reference count to manage memory.

Value Types: Structures, enums, and basic types (e.g., Int, String) are value types. Assigning or passing them creates a copy, so changes to one instance don’t affect others. ARC doesn’t manage value types since each copy has its own memory allocation.

ARC in Action

Here’s an example of how Automatic Reference Counting works where we create a class, instantiate it, and observe how ARC manages memory and deinitializes the instance when it is no longer needed.

class Person {
var name: String

init(name: String) {
self.name = name
print("\(name) is being initialized")
}

deinit {
print("\(name) is being deinitialized")
}
}

// Create a new instance of the Person class
var a: Person? = Person(name: "John") // Reference count of instance is now 1
// Prints "John is being initialized"

var b: Person? = a // Reference count increases to 2
var c: Person? = a // Reference count increases to 2

// At this point, the reference count for the "John" instance is 3
// (a, b, and c are all pointing to the same instance)

We declared a class named Person and created an instance of the Person class (a = Person(name: "John")), ARC assigns a reference count of 1 to that instance. When you assign that same instance to b and c variables, the reference count increases to 3 because there are now three references to the same instance.

a = nil // decreases the reference count by 1, remaining 2
b = nil // decreases the reference count by 1, remaining 1
c = nil // decreases the reference count by 1, remaining 0

// Prints "John is being deinitialized"

The reference count drops to 0, so ARC deallocates the “John” instance and the `deinit` method is called.

This is ARC in action, automatically managing memory without requiring the developer to manually free or allocate memory. Okay. So now we know how ARC works. But will this always be simple?

Let’s take another example.

class Person {
var name: String
var apartment: Apartment?

init(name: String) {
self.name = name
}

deinit {
print("\(name) is being deinitialized")
}
}

class Apartment {
var tenant: Person?

init(unit: String) {
self.unit = unit
}

deinit {
print("Apartment is being deinitialized")
}
}


var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

In the above code, we have created two classes named Person and Apartment. Every Person instance has a name property of type String and an optional apartment property that’s initially nil. Similarly, every Apartment instance has a unit property of type String and has an optional tenant property that’s initially nil.

Both of these classes also define a deinitializer, which prints the fact that an instance of that class is being deinitialized. This enables you to see whether instances of Person and Apartment are being deallocated as expected.

At the end, we created a specific Person instance and Apartment instance and assign these new instances to the john and unit4A variables. Here’s how the strong references look after creating and assigning these two instances. The john variable now has a strong reference to the new Person instance, and the unit4A variable has a strong reference to the new Apartment instance.

Source: https://docs.swift.org

Now link the two instances together so that the person has an apartment, and the apartment has a tenant.

john!.apartment = unit4A
unit4A!.tenant = john

Here’s how the strong references look after you link the two instances together. Both class Person & Apartment instances refer to each other with strong reference.

Now, let’s set the john and unit4A variables to nil. You will notice that you won’t see any print statement from the deinit i.e. both the instance will not get deinitialized. Let’s assess what is happening here.

When I set the var john to nil, instance Person will not get de-initialized as instance Apartment still holds a reference to instance Person, i.e. the reference count of Person instance won’t drop to zero.

Setting unit4A to nil won’t be able to de-initialize instance Apartment as instance Person which has not yet been de-initialized still holds a reference to instance Apartment.

The strong reference cycle prevents the Person and Apartment instances from ever being deallocated, causing a memory leak in your app. The above case is an example of Retain Cycle.

A retain cycle occurs when two or more class instances references to each other strongly, preventing them from being deallocated, thus resulting in memory leaks as the memory occupied by the instances is never freed.

Resolving Retain Cycle

Swift provides two ways to resolve strong reference cycles when you work with properties of class type: weak references and unowned references.
But the selection of weak or unowned depends upon the relation and life cycle of both instances.

Strong, Weak, and Unowned References

Strong References

Strong references prevent an object from being deallocated, meaning the object will not be deallocated as long as there is another strong reference to it. This is the default reference type.

class Person {
var name: String
init(name: String) {
self.name = name
}
}

var person1: Person? = Person(name: "John Appleseed") // Strong reference

Weak References

A weak reference doesn’t keep a strong hold on the instance. A weak reference is always defined as an optional type, as ARC automatically sets the reference to nil when the instance gets deallocated.

You indicate a weak reference by placing the weak keyword before a property or variable declaration.

The example below is identical to Person and Apartment example from above, with one important difference. This time around, the Apartment type’s tenant property is declared as a weak reference:

class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}


class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

Here’s how the references look now that you’ve linked the two instances together:

The Person instance still has a strong reference to the Apartment instance, but the Apartment instance now has a weak reference to the Person instance. This means that when you break the strong reference held by the john variable by setting it to nil, there are no more strong references to the Person instance:

john = nil
// Prints "John Appleseed is being deinitialized"

Because there are no more strong references to the Person instance, it’s deallocated and the tenant property is set to nil:

The only remaining strong reference to the Apartment instance is from the unit4A variable. If you break that strong reference, there are no more strong references to the Apartment instance:

unit4A = nil
// Prints "Apartment 4A is being deinitialized"

Because there are no more strong references to the Apartment instance, it too is deallocated.

Unowned References

Like a weak reference, an unowned reference doesn’t keep a strong hold on the instance it refers to. However, they are used when you are certain that the reference will never be nil once it is set, making them non-optional.

Trying to access an unowned reference after the instance is deallocated will result in a runtime crash.

class Person {
var name: String
var job: Job? // Strong reference to Job

init(name: String) { self.name = name }

deinit {
print("\(name) is being deinitialized")
}
}

class Job {
unowned var employee: Person // Unowned reference to Person

init(employee: Person) { self.employee = employee }

deinit {
print("Job for \(employee.name) is being deinitialized")
}
}



var john: Person? = Person(name: "John")
john?.job = Job(employee: john!) // setting the references

john = nil
// When 'john' is set to nil, the Person and Job are both deallocated,
// and the unowned reference ensures that there’s no retain cycle.


// Prints "John is being deinitialized"
// Prints "Job for John is being deinitialized"

The Job class holds an unowned reference to the Person (employee). This means that the Job does not increase the reference count of Person, but it also expects Person to always exist as long as the Job exists. If Person is deallocated, the Job will be deallocated as well.

The above code will work just fine. But what if we change the order?

When we set john to nil, instance Person gets de-initialized. Now if we try to access the reference from Job instance, it will cause a run time exception because unowned references do not become nil.

Retain Cycle in Closures

Closures in Swift can also cause retain cycles when they capture self strongly. To avoid this, you use a capture list to declare weak or unowned references to self

class ViewController {
var title: String = "Main Screen"
var onButtonTap: (() -> Void)?

func setupButton() {
onButtonTap = {
print("Button tapped, showing \(self.title)")
}
}

deinit {
print("ViewController is being deinitialized")
}
}


var vc: ViewController? = ViewController()
vc?.setupButton() // Set up the closure

vc = nil // ViewController will not be deallocated due to a retain cycle

The onButtonTap closure holds a strong reference to self (ViewController), and ViewController holds a strong reference to the closure through its onButtonTap property. This creates a retain cycle.

When you set vc to nil, ViewController is not deallocated because both the closure and the ViewController hold strong references to each other.

Fixing the Retain Cycle Using [weak self] or [unowned self]

class ViewController {
var title: String = "Main Screen"
var onButtonTap: (() -> Void)?

func setupButton() {
onButtonTap = { [weak self] in
guard let strongSelf = self else {
print("ViewController is no longer available.")
return
}
print("Button tapped, showing \(strongSelf.title)")
}
}

deinit {
print("ViewController is being deinitialized")
}
}


var vc: ViewController? = ViewController()
vc?.setupButton() // et up the closure

vc = nil
// Now, ViewController will be deallocated
// because we have broken the retain cycle

// prints "ViewController is being deinitialized"

By using [weak self], the closure captures a weak reference to self. This prevents the closure from keeping self alive, thus breaking the retain cycle. Also, before using self inside the closure, we check if self is still available by safely unwrapping it with guard let.

Conclusion

Understanding ARC and memory management in Swift is vital for building efficient and reliable iOS applications. By using strong, weak, and unowned references correctly, we can avoid common pitfalls such as retain cycles and memory leaks.

Thanks for Reading!

Hope, you have found this article useful. If you liked this article, please share and 👏 so other people can read it too.

Follow me at LinkedIn: linkedin.com/in/rohit-13/

For reading & learning about these topics, I have found the following articles very useful :-

  1. https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/
  2. https://medium.com/@quasaryy/differences-between-weak-and-unowned-in-swift-with-examples-d6a54357dd1c

--

--

Rohit Kumar
Rohit Kumar

Written by Rohit Kumar

SDE @Yellow.ai | Swift, UIKIt, SwiftUI

No responses yet