Skip to main content

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:

swift
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:

swift
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:

  1. Weak References - Marked with weak
  2. 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.

swift
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.

swift
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

  1. Strong Reference (default): Use when you want to keep an object in memory as long as there's a reference to it
  2. Weak Reference: Use when references might become nil at some point and the order of deallocation isn't guaranteed
  3. 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:

swift
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:

swift
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 become nil
  • 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

  1. Create a class hierarchy representing a social network where User objects can follow other User objects. Implement proper reference counting to avoid memory leaks.
  2. Implement a notification system where objects can subscribe to notifications but are automatically unsubscribed when deallocated.
  3. 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! :)