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.
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.
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, instancePerson
gets de-initialized. Now if we try to access the reference fromJob
instance, it will cause a run time exception becauseunowned
references do not becomenil
.
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 :-