Swift Reference Counting
When working with classes in Swift, understanding how memory is managed is crucial for building efficient applications. Unlike structs and enums, which are value types, classes are reference types and their memory management is handled through a mechanism called Automatic Reference Counting (ARC).
What is Reference Counting?
Reference counting is Swift's memory management system that tracks how many references exist to a class instance. When the count drops to zero (meaning no part of your app references that object anymore), Swift automatically deallocates the instance to free up memory.
Unlike other programming languages that use garbage collection (which periodically scans for unused objects), Swift's ARC is deterministic and works during compilation time, making memory management more predictable.
How ARC Works
When you create a new instance of a class, ARC allocates memory to store information about that instance. When the instance is no longer needed, ARC frees up this memory.
Let's see a basic 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 reference to a new Person instance
var reference1: Person? = Person(name: "John")
// Output: John is being initialized
// Create another reference to the same Person instance
var reference2 = reference1
var reference3 = reference1
// Break the first reference
reference1 = nil
// Nothing happens yet because reference2 and reference3 still point to the instance
// Break the second reference
reference2 = nil
// Still nothing happens
// Break the final reference
reference3 = nil
// Output: John is being deinitialized
In this example, the Person
instance is only deinitialized when all three references are set to nil
, causing the reference count to drop to zero.
Strong Reference Cycles
One of the challenges with ARC is dealing with strong reference cycles (also known as retain cycles). These happen when two class instances hold strong references to each other, causing neither to be deallocated even when they're no longer needed.
Consider this example:
class Teacher {
let name: String
var student: Student?
init(name: String) {
self.name = name
print("Teacher \(name) initialized")
}
deinit {
print("Teacher \(name) deinitialized")
}
}
class Student {
let name: String
var teacher: Teacher?
init(name: String) {
self.name = name
print("Student \(name) initialized")
}
deinit {
print("Student \(name) deinitialized")
}
}
// Create instances and establish relationships
var teacher: Teacher? = Teacher(name: "Mrs. Smith")
var student: Student? = Student(name: "Bob")
teacher?.student = student
student?.teacher = teacher
// Try to break references
teacher = nil
student = nil
// No deinitialization happens - memory leak!
In this example, even though we set both teacher
and student
to nil
, the instances aren't deallocated because they still hold strong references to each other.
Solving Strong Reference Cycles
Swift provides two ways to solve strong reference cycles:
- Weak References - Marked with
weak
- Unowned References - Marked with
unowned
Weak References
A weak reference is one that doesn't keep a strong hold on the instance it refers to. When the referenced instance is deallocated, the weak reference is automatically set to nil
.
class Teacher {
let name: String
var student: Student?
init(name: String) {
self.name = name
print("Teacher \(name) initialized")
}
deinit {
print("Teacher \(name) deinitialized")
}
}
class Student {
let name: String
weak var teacher: Teacher?
init(name: String) {
self.name = name
print("Student \(name) initialized")
}
deinit {
print("Student \(name) deinitialized")
}
}
// Create instances and establish relationships
var teacher: Teacher? = Teacher(name: "Mrs. Smith")
var student: Student? = Student(name: "Bob")
teacher?.student = student
student?.teacher = teacher
// Break references
teacher = nil
// Output: Teacher Mrs. Smith deinitialized
student = nil
// Output: Student Bob deinitialized
By marking the teacher
property in the Student
class as weak
, we break the strong reference cycle. Note that weak references must always be declared as optional variables (var
) because they're automatically set to nil
when the referenced instance is deallocated.
Unowned References
An unowned reference is similar to a weak reference but is used when the reference is expected to always have a value. Unlike weak references, unowned references aren't optionals.
class CreditCard {
let number: String
unowned let customer: Customer
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
print("Credit card \(number) initialized")
}
deinit {
print("Credit card \(number) deinitialized")
}
}
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
print("Customer \(name) initialized")
}
deinit {
print("Customer \(name) deinitialized")
}
}
// Create customer first, since CreditCard needs a customer
var customer: Customer? = Customer(name: "Alice")
// Output: Customer Alice initialized
// Create credit card with a reference to the customer
customer?.card = CreditCard(number: "1234-5678", customer: customer!)
// Output: Credit card 1234-5678 initialized
// Break the reference to the customer
customer = nil
// Output:
// Credit card 1234-5678 deinitialized
// Customer Alice deinitialized
In this example, a CreditCard
instance always has a reference to its owner (the Customer
), so we use an unowned reference. When the customer reference becomes nil
, both instances are deallocated properly.
When to Use Each Type of Reference
- Strong Reference (default): Use when you want to keep an object in memory as long as there's a reference to it
- Weak Reference: Use when references might become
nil
at some point and the order of deallocation isn't guaranteed - Unowned Reference: Use when you know the reference will never be
nil
during its lifetime, and it has a shorter lifetime than the object that holds it
Practical Example: Delegate Pattern
A common use case for weak references is the delegate pattern, which is widely used in iOS development:
protocol ChatDelegate: AnyObject {
func didReceiveMessage(_ message: String)
}
class ChatRoom {
weak var delegate: ChatDelegate?
func receiveMessage(from sender: String, text: String) {
let message = "\(sender): \(text)"
delegate?.didReceiveMessage(message)
}
}
class UserInterface: ChatDelegate {
let name: String
var chatRoom: ChatRoom?
init(name: String) {
self.name = name
print("UI for \(name) initialized")
}
deinit {
print("UI for \(name) deinitialized")
}
func didReceiveMessage(_ message: String) {
print("Displaying: \(message)")
}
func setupChat() {
let room = ChatRoom()
room.delegate = self
self.chatRoom = room
}
}
// Create a UI and setup chat
var ui: UserInterface? = UserInterface(name: "User")
// Output: UI for User initialized
ui?.setupChat()
ui?.chatRoom?.receiveMessage(from: "System", text: "Welcome!")
// Output: Displaying: System: Welcome!
// Break the reference
ui = nil
// Output: UI for User deinitialized
In this example, the ChatRoom
holds a weak reference to its delegate to avoid creating a strong reference cycle between the ChatRoom
and the UserInterface
.
Capture Lists in Closures
Closures can also create strong reference cycles if they capture references to objects. To prevent this, Swift provides capture lists:
class ViewModel {
var title: String
var onUpdate: (() -> Void)?
init(title: String) {
self.title = title
print("ViewModel '\(title)' initialized")
}
deinit {
print("ViewModel '\(title)' deinitialized")
}
func setupClosures() {
// This would create a strong reference cycle
// onUpdate = { self.title = "Updated" }
// Use a capture list to avoid the cycle
onUpdate = { [weak self] in
guard let self = self else { return }
self.title = "Updated"
print("Title updated to: \(self.title)")
}
}
}
var viewModel: ViewModel? = ViewModel(title: "Home Screen")
// Output: ViewModel 'Home Screen' initialized
viewModel?.setupClosures()
viewModel?.onUpdate?()
// Output: Title updated to: Updated
// Break the reference
viewModel = nil
// Output: ViewModel 'Updated' deinitialized
Without the [weak self]
capture list, the closure would strongly capture self
, creating a reference cycle because ViewModel
strongly holds the closure while the closure strongly holds the ViewModel
.
Summary
Automatic Reference Counting (ARC) is Swift's memory management system that automatically tracks and manages memory usage in your app. Key points to remember:
- ARC increases the reference count when you create a reference to a class instance and decreases it when references are removed
- When the reference count reaches zero, the instance is deallocated
- Strong reference cycles can occur when two instances strongly reference each other
- Use
weak
references for optional references that can becomenil
- Use
unowned
references when you're sure the reference will always have a value during its lifetime - Capture lists (
[weak self]
or[unowned self]
) help prevent reference cycles in closures
Understanding reference counting in Swift is essential for building applications that efficiently manage memory and avoid memory leaks.
Exercises
- Create a class hierarchy representing a social network where
User
objects can follow otherUser
objects. Implement proper reference counting to avoid memory leaks. - Implement a notification system where objects can subscribe to notifications but are automatically unsubscribed when deallocated.
- Refactor a closure-based callback system to use weak references appropriately.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)