Swift ARC Basics
Introduction
Memory management is a crucial aspect of programming that ensures your application uses resources efficiently. In Swift, memory management is handled through a system called Automatic Reference Counting (ARC). Unlike other programming languages that use garbage collectors, Swift's ARC determines when class instances are no longer needed and automatically frees up the memory they occupy.
In this lesson, we'll explore how ARC works in Swift, why it's important, and how to avoid common memory management issues like memory leaks.
What is Automatic Reference Counting?
ARC is a memory management feature in Swift that tracks and manages your app's memory usage. When you create an instance of a class, ARC allocates a chunk of memory to store information about that instance. When the instance is no longer needed, ARC frees up the memory used by that instance so that the memory can be used for other purposes.
Key Concept: References
ARC works by keeping track of how many references exist to each class instance. As long as at least one active reference to an instance exists, ARC keeps that instance in memory. When no active references remain, ARC deallocates the instance.
Let's see how this works with a simple example:
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
// Create a new scope
do {
let person = Person(name: "John")
print("Person object is available inside this scope")
}
// The scope ends here
print("The scope has ended")
Output:
John is being initialized
Person object is available inside this scope
John is being deinitialized
The scope has ended
In this example:
- We create a
Person
class with an initializer and a deinitializer (marked by thedeinit
keyword). - Inside the
do
block, we create an instance ofPerson
named "John". - When the
do
block ends, theperson
variable goes out of scope, meaning there are no more references to ourPerson
instance. - ARC automatically deallocates the instance, triggering the deinitializer.
Strong References
By default, any time you assign a class instance to a property, constant, or variable, a strong reference is created. A strong reference says, "Keep this object in memory as long as I exist."
Strong Reference Cycles (Memory Leaks)
One challenge with ARC is the possibility of creating strong reference cycles (also known as retain cycles or memory leaks). This happens when two class instances hold strong references to each other, creating a cycle that prevents ARC from deallocating either instance.
Let's see an example of a strong reference cycle:
class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
class Apartment {
let unit: String
var tenant: Person?
init(unit: String) {
self.unit = unit
print("Apartment \(unit) is being initialized")
}
deinit {
print("Apartment \(unit) is being deinitialized")
}
}
// Create a strong reference cycle
do {
let john = Person(name: "John")
let unit4A = Apartment(unit: "4A")
john.apartment = unit4A // Person now has a strong reference to Apartment
unit4A.tenant = john // Apartment now has a strong reference to Person
}
print("The scope has ended")
Output:
John is being initialized
Apartment 4A is being initialized
The scope has ended
Notice that the deinitializers are never called! Even though john
and unit4A
go out of scope, each instance maintains a strong reference to the other, preventing ARC from deallocating either object. This is a memory leak.
Breaking Strong Reference Cycles
Swift provides two ways to resolve strong reference cycles:
- Weak references
- Unowned references
Weak References
A weak reference does not keep an object alive. If the object it points to is deallocated, the weak reference is automatically set to nil
. Weak references must be optional variables because they can become nil
at any time.
Let's fix our previous example using a weak reference:
class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
class Apartment {
let unit: String
weak var tenant: Person? // Using weak reference
init(unit: String) {
self.unit = unit
print("Apartment \(unit) is being initialized")
}
deinit {
print("Apartment \(unit) is being deinitialized")
}
}
do {
let john = Person(name: "John")
let unit4A = Apartment(unit: "4A")
john.apartment = unit4A
unit4A.tenant = john
}
print("The scope has ended")
Output:
John is being initialized
Apartment 4A is being initialized
John is being deinitialized
Apartment 4A is being deinitialized
The scope has ended
Now both instances are properly deallocated when they go out of scope.
Unowned References
An unowned reference is similar to a weak reference but must always refer to an instance that has not been deallocated. Unlike weak references, unowned references are not optional and cannot be nil. Use unowned when the referenced instance will always have the same or longer lifetime than the instance that references it.
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
class CreditCard {
let number: String
unowned let customer: Customer // Using unowned reference
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
print("Card \(number) is being initialized")
}
deinit {
print("Card \(number) is being deinitialized")
}
}
do {
let john = Customer(name: "John")
john.card = CreditCard(number: "1234-5678-9012-3456", customer: john)
}
print("The scope has ended")
Output:
John is being initialized
Card 1234-5678-9012-3456 is being initialized
Card 1234-5678-9012-3456 is being deinitialized
John is being deinitialized
The scope has ended
In this case, we use unowned
because a credit card will always have a customer, and we can assume the customer won't be deallocated before their credit card.
When to Use Weak vs. Unowned References
Use weak when:
- The reference might become
nil
at some point - The referenced object might be deallocated while your object still exists
- Common cases: delegate patterns, parent-child relationships where the parent doesn't own the child
Use unowned when:
- The reference will never be
nil
once set - The referenced object will always have a longer or equal lifetime
- Common cases: part-whole relationships where one object is a logical part of another
Practical Example: Delegate Pattern
The delegate pattern is one of the most common places where weak references are needed in iOS development:
protocol DataUpdaterDelegate: AnyObject {
func didUpdateData(_ data: String)
}
class DataUpdater {
weak var delegate: DataUpdaterDelegate?
func fetchData() {
// Simulate network fetch
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.delegate?.didUpdateData("Fresh data from server")
}
}
}
class DataPresenter: DataUpdaterDelegate {
let dataUpdater = DataUpdater()
init() {
dataUpdater.delegate = self
}
func startFetching() {
dataUpdater.fetchData()
}
func didUpdateData(_ data: String) {
print("Received data: \(data)")
}
}
// Usage
let presenter = DataPresenter()
presenter.startFetching()
// After 1 second: "Received data: Fresh data from server"
In this example, we use a weak reference for the delegate to avoid a strong reference cycle between DataUpdater
and DataPresenter
.
Summary
Automatic Reference Counting (ARC) is Swift's memory management system that works behind the scenes to allocate and deallocate memory for your app. Key points to remember:
- ARC tracks strong references to class instances
- When the count of strong references to an instance drops to zero, ARC deallocates the instance
- Strong reference cycles occur when two objects reference each other strongly, preventing deallocation
- Weak and unowned references help break reference cycles
- Use
weak
for references that can become nil - Use
unowned
for references that will never become nil during their lifetime
- Use
Understanding ARC is essential for writing efficient Swift applications that don't leak memory. By being mindful of your reference relationships, you can ensure your app uses memory efficiently.
Exercises
-
Create a parent-child relationship between two classes where the parent has an array of children and the children have a reference back to the parent. Use appropriate reference types to avoid memory leaks.
-
Implement a notification system where objects can subscribe to receive notifications. Make sure that when subscriber objects are deallocated, they are automatically removed from the notification list.
-
Identify and fix the memory leak in the following code:
swiftclass NetworkManager {
var completionHandler: (() -> Void)?
func performRequest(completion: @escaping () -> Void) {
self.completionHandler = {
print("Request completed")
completion()
}
}
}
class ViewController {
let networkManager = NetworkManager()
func fetchData() {
networkManager.performRequest {
self.updateUI()
}
}
func updateUI() {
print("UI Updated")
}
}
Additional Resources
- Swift Documentation on ARC
- WWDC Session: Embracing Algorithms - Includes discussion on memory management
- Practical Memory Management in Swift by Swift by Sundell
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)