ARC and Memory Management in Swift
In this tutorial, you’ll learn how ARC works and how to code in Swift for optimal memory management. You’ll learn what reference cycles are, how to use the Xcode 10 visual debugger to discover them when they happen and how to break them using an example of a reference cycle in practice. By Mark Struzinski.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
ARC and Memory Management in Swift
30 mins
- Getting Started
- Is That in Scope?
- An Object’s Lifetime
- Reference Cycles
- Checking Your References
- Hold the Phone(s)
- Weak References
- Unowned References
- Who’s Your Carrier?
- Break the Chain
- Reference Cycles With Closures
- Capture Lists
- Capture Your Self
- Using Unowned With Care
- Disarming the Trap
- Now for Something Different
- Finding Reference Cycles in Xcode 10
- Where Is That Leak?
- Bonus: Cycles With Value Types and Reference Types
- Reference and Value
- Where to Go From Here?
As a modern, high-level programming language, Swift handles much of the memory management of your apps and allocates or deallocates memory on your behalf. It does so using a feature of the Clang compiler called Automatic Reference Counting, or ARC. In this tutorial, you’ll learn all about ARC and memory management in Swift.
With an understanding of this system, you can influence when the life of a heap object ends. Swift uses ARC to be predictable and efficient in resource-constrained environments.
ARC works automatically, so you don’t need to participate in reference counting, but you do need to consider relationships between objects to avoid memory leaks. This is an important requirement that is often overlooked by new developers.
In this tutorial, you’ll level up your Swift and ARC skills by learning the following:
- How ARC works.
- What reference cycles are and how to break them.
- An example of a reference cycle in practice.
- How to detect reference cycles with the latest Xcode visualization tools.
- How to deal with mixed value and reference types.
Getting Started
Click the Download Materials button at the top or bottom of this tutorial. In the folder named Cycles, open the starter project. For the first part of this tutorial, you’ll be working completely inside MainViewController.swift to learn some core concepts.
Add the following class to the bottom of MainViewController.swift:
class User {
let name: String
init(name: String) {
self.name = name
print("User \(name) was initialized")
}
deinit {
print("Deallocating user named: \(name)")
}
}
This defines a class User
which has print
statements to show when you have initialized or deallocated it.
Now, initialize an instance of User
at the top of MainViewController
.
Put the following code above viewDidLoad()
:
let user = User(name: "John")
Build and run the app. Make sure the console is visible with Command-Shift-Y so you can see the result of the print
statements.
Notice that the console shows User John was initialized and that the print
within deinit
is never called. This means that the object is never deallocated because it never goes out of scope.
In other words, since the view controller that contains this object never goes out of scope, the object is never removed from memory.
Is That in Scope?
Wrapping the instance of user
in a method will allow it to go out of scope, letting ARC deallocate it.
Create a method called runScenario()
inside the MainViewController
class. Move the initialization of User
inside of it.
func runScenario() {
let user = User(name: "John")
}
runScenario()
defines the scope for the instance of User
. At the end of this scope, user
should be deallocated.
Now, call runScenario()
by adding the following at the end of viewDidLoad()
:
runScenario()
Build and run again. The console output now looks like this:
User John was initialized
Deallocating user named: John
The initialization and deallocation print
statements both appear. These statements show that you’ve deallocated the object at the end of the scope.
An Object’s Lifetime
The lifetime of a Swift object consists of five stages:
- Allocation: Takes memory from a stack or heap.
-
Initialization:
init
code runs. - Usage.
-
Deinitialization:
deinit
code runs. - Deallocation: Returns memory to a stack or heap.
There are no direct hooks into allocation and deallocation, but you can use print
statements in init
and deinit
as a proxy for monitoring those processes.
Reference counts, also known as usage counts, determine when an object is no longer needed. This count indicates how many “things” reference the object. The object is no longer needed when its usage count reaches zero and no clients of the object remain. The object then deinitializes and deallocates.
When you initialize the User
object, it starts with a reference count of one, since the constant user
references that object.
At the end of runScenario()
, user
goes out of scope and the reference count decrements to zero. As a result, user
deinitializes and subsequently deallocates.
Reference Cycles
In most cases, ARC works like a charm. As an app developer, you don’t usually have to worry about memory leaks, where unused objects stay alive indefinitely.
But it’s not all smooth sailing. Leaks can happen!
How can these leaks occur? Imagine a situation where two objects are no longer required, but each references the other. Since each has a non-zero reference count, neither object can deallocate.
This is a strong reference cycle. It fools ARC and prevents it from cleaning up.
As you can see, the reference count at the end is not zero, and even though neither is still required, object1
and object2
are never deallocated.
Checking Your References
To see this in action, add the following code after User
in MainViewController.swift:
class Phone {
let model: String
var owner: User?
init(model: String) {
self.model = model
print("Phone \(model) was initialized")
}
deinit {
print("Deallocating phone named: \(model)")
}
}
This adds a new class called Phone
. It has two properties, one for the model and one for the owner, with init
and deinit
methods. The owner
property is optional, since a Phone
can exist without a User
.
Next add the following line to runScenario()
:
let iPhone = Phone(model: "iPhone Xs")
This creates an instance of Phone
.
Hold the Phone(s)
Next, add the following code to User
, immediately after the name
property:
private(set) var phones: [Phone] = []
func add(phone: Phone) {
phones.append(phone)
phone.owner = self
}
This adds a phones
array property to hold all phones owned by a user. The setter is private, so clients have to use add(phone:)
. This method ensures that owner
is set properly when you add it.
Build and run. As you can see in the console, the Phone
and User
objects deallocate as expected.
User John was initialized
Phone iPhone XS was initialized
Deallocating phone named: iPhone Xs
Deallocating user named: John
Now, add the following at the end of runScenario()
:
user.add(phone: iPhone)
Here, you add iPhone
to user
. add(phone:)
also sets the owner
property of iPhone
to user
.
Now build and run, and you’ll see user
and iPhone
do not deallocate. A strong reference cycle between the two objects prevents ARC from deallocating either of them.