Understanding Unowned References in Swift
Swift's Automatic Reference Counting (ARC) is a powerful memory management system that frees developers from manual memory handling. Most of the time, ARC "just works," automatically deallocating objects when they are no longer needed. However, there are specific scenarios, particularly involving strong reference cycles, where ARC needs a little help. This is where weak and unowned references come into play.
You're probably familiar with weak references, often used in delegate patterns or when dealing with parent-child relationships where the child might outlive the parent. But what about unowned? While unowned also helps break strong reference cycles, it operates under a different set of assumptions and comes with its own unique implications. Misunderstanding the distinction between weak and unowned can lead to subtle bugs or even crashes in your application.
In this article, we'll take a deep dive into unowned references: what they are, when to use them, how they differ from weak references, and the critical pitfalls to avoid.
The Problem: Strong Reference Cycles
A strong reference cycle, also known as a retain cycle, occurs when two or more objects hold strong references to each other, preventing them from being deallocated by ARC. Even if there are no other strong references to these objects from outside the cycle, ARC cannot free them because their reference counts never drop to zero. This leads to a memory leak, where memory is consumed indefinitely by objects that are no longer accessible or needed.
Consider a simple example: a Customer and an Account.
class Customer {
let name: String
var account: Account?
init(name: String) {
self.name = name
print("Customer \(name) initialized.")
}
deinit {
print("Customer \(name) deinitialized.")
}
}
class Account {
let accountNumber: String
var owner: Customer? // Strong reference back to Customer
init(accountNumber: String) {
self.accountNumber = accountNumber
print("Account \(accountNumber) initialized.")
}
deinit {
print("Account \(accountNumber) deinitialized.")
}
}
var rahul: Customer?
var savingsAccount: Account?
rahul = Customer(name: "Rahul")
savingsAccount = Account(accountNumber: "12345")
rahul?.account = savingsAccount
savingsAccount?.owner = rahul // This creates a strong reference cycle!
print("Setting nils to break references...")
rahul = nil
savingsAccount = nil
print("References set to nil.")
If you run this code, you'll see:
Customer Rahul initialized.
Account 12345 initialized.
Setting nils to break references...
References set to nil.
Notice that neither Customer Rahul deinitialized. nor Account 12345 deinitialized. is printed. This indicates a memory leak. rahul and savingsAccount both have a strong reference count of 1 from each other, preventing ARC from deallocating them even after our external rahul and savingsAccount variables are set to nil.
Breaking the Cycle with unowned References
To fix this, we need to break one of the strong references in the cycle. This is where weak and unowned references come in.
When to use unowned vs. weak
The choice between weak and unowned hinges on the lifetime relationship between the two objects involved in the potential cycle:
weak: Useweakwhen the referenced instance might becomenilat some point during its lifetime. This typically occurs when the referencing object has a shorter or independent lifetime compared to the referenced object. Aweakreference is always an optional type, and ARC automatically sets it tonilwhen the object it refers to is deallocated. This makes it safe to check fornilbefore accessing the weak reference.
unowned: Useunownedwhen the referenced instance will always have a value during the referencing instance's lifetime. In other words, theunownedreference assumes that the object it refers to will never benilwhile theunownedreference itself is still alive. Because of this guarantee, anunownedreference is defined as a non-optional type.
Applying unowned to the Customer-Account Example
In our Customer-Account scenario, it's reasonable to assume that an Account always has an owner (Customer) for the entire lifetime of the Account itself. An Account can't exist without an owner, and if the owner is deallocated, the Account should also be deallocated (or at least its owner property would become invalid). In this specific relationship, the Customer "owns" the Account, implying the Customer will outlive or have the same lifetime as the Account.
Let's modify the Account class to use an unowned reference for its owner:
class Customer {
let name: String
var account: Account?
init(name: String) {
self.name = name
print("Customer \(name) initialized.")
}
deinit {
print("Customer \(name) deinitialized.")
}
}
class Account {
let accountNumber: String
unowned var owner: Customer // Unowned reference!
init(accountNumber: String, owner: Customer) { // Owner must be provided on init
self.accountNumber = accountNumber
self.owner = owner
print("Account \(accountNumber) initialized.")
}
deinit {
print("Account \(accountNumber) deinitialized.")
}
}
var rahul: Customer?
var savingsAccount: Account?
rahul = Customer(name: "Rahul")
// Now, owner must be passed during Account initialization
savingsAccount = Account(accountNumber: "12345", owner: rahul!)
rahul?.account = savingsAccount
print("Setting nils to break references...")
rahul = nil
savingsAccount = nil
print("References set to nil.")
Now, the output will be:
Customer Rahul initialized.
Account 12345 initialized.
Setting nils to break references...
Customer Rahul deinitialized.
Account 12345 deinitialized.
References set to nil.
Success! Both objects are deinitialized correctly. The Account's unowned owner reference does not contribute to the Customer's strong reference count, thus breaking the cycle.
┌───────────────┐ ┌───────────────┐
│ Customer │ ──► │ Account │
│ (strong) │ │ (strong) │
└───────────────┘ └───────────────┘
▲ │
│ │
└──────────(unowned)┴
unowned References in Closures
Another common scenario for unowned references is within closure capture lists, especially when self is guaranteed to exist for the lifetime of the closure. This often happens when a closure is stored as a property on an object, and that closure refers back to self.
Consider a TimerManager that holds a closure to be executed periodically:
class TimerManager {
var timer: Timer?
var message: String
init(message: String) {
self.message = message
print("TimerManager initialized with message: \"\(message)\"")
}
func startTimer() {
// Here, the closure captures `self`.
// If the closure is stored as a property of `self`,
// it creates a strong reference cycle.
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [unowned self] _ in
print("\(self.message) - Tick!")
}
}
func stopTimer() {
timer?.invalidate()
timer = nil
print("Timer stopped.")
}
deinit {
stopTimer() // Ensure timer is invalidated on deinit
print("TimerManager deinitialized.")
}
}
var manager: TimerManager? = TimerManager(message: "Hello")
manager?.startTimer()
// Let the timer run for a bit
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
print("Attempting to deallocate TimerManager...")
manager = nil // This would cause a leak without `unowned self`
}
Without [unowned self], the closure would strongly capture manager, and manager would strongly hold the timer (which holds the closure), creating a cycle. By using [unowned self], we tell ARC that the TimerManager instance will always be alive as long as the closure itself is alive. When manager = nil is executed, the TimerManager instance's strong reference count drops to zero, allowing it to deallocate, which in turn invalidates the timer and releases the closure.
The output would show TimerManager deinitialized. after 3 seconds, proving the cycle is broken.
The Dangers of Misusing unowned
The "guaranteed to exist" assumption of unowned references is powerful, but also dangerous if violated. If an unowned reference tries to access an object that has already been deallocated, your application will crash with a runtime error. This is because unowned references are non-optional and don't perform the nil check that weak references do.
Consider a scenario where the owner might indeed be deallocated before the Account:
class BadCustomer {
let name: String
var account: BadAccount?
init(name: String) { self.name = name }
deinit { print("BadCustomer \(name) deinitialized.") }
}
class BadAccount {
let accountNumber: String
unowned var owner: BadCustomer // DANGER: What if owner is nil?
init(accountNumber: String) { self.accountNumber = accountNumber }
deinit { print("BadAccount \(accountNumber) deinitialized.") }
}
var badRahul: BadCustomer? = BadCustomer(name: "Bad Rahul")
var badAccount: BadAccount? = BadAccount(accountNumber: "999")
badRahul?.account = badAccount
badAccount?.owner = badRahul! // Force unwrap here because owner is non-optional
// What if badRahul is set to nil BEFORE badAccount?
badRahul = nil // BadCustomer is deinitialized here
// Now, badAccount's 'owner' property points to deallocated memory.
// Accessing it will cause a crash!
// print(badAccount?.owner.name) // This line would crash the app!
badAccount = nil // BadAccount deinitialized (if it wasn't already crashed)
In this contrived example, if badRahul is deallocated first, badAccount.owner becomes a dangling pointer. Any subsequent attempt to access badAccount.owner (e.g., badAccount?.owner.name) would result in a runtime crash, often an EXC_BAD_ACCESS. This is why understanding the lifetime relationship is paramount.
unowned(safe) vs. unowned(unsafe)
Swift's unowned keyword actually implies unowned(safe). This "safe" version performs runtime checks to ensure the instance it refers to is still alive. If it's not, it triggers a runtime error (crash), as shown above.
There's also an unowned(unsafe) variant. This version skips the runtime check, making it slightly faster but much more dangerous. If you access an unowned(unsafe) reference to a deallocated instance, it will lead to undefined behavior, which can be much harder to debug than a direct crash. For almost all practical Swift development, you should stick to the default unowned (i.e., unowned(safe)).
unowned vs. weak in Summary
To reiterate the key differences:
- When to use
weak: Chooseweakwhen the relationship is such that the "child" or "delegate" object might be deallocated independently of its "parent" or "delegating" object. For instance, aViewControllermight hold aweakreference to aCoordinatorif theCoordinatoris managed by a higher-level object and could be deallocated while theViewControlleris still in memory (though this specific example depends heavily on the Coordinator pattern implementation). Another common case is a delegate that can be set and unset, or might simply disappear.
- When to use
unowned: Opt forunownedwhen you are absolutely certain that the referenced object will live at least as long as the object holding theunownedreference. This implies a strong, non-optional, dependent relationship where one object cannot logically exist without the other, or where its existence guarantees the other's. TheCustomerandAccountexample (where anAccountinherently belongs to aCustomer) is a perfect fit.
Summary
unowned references are a powerful tool in Swift's memory management arsenal, designed to prevent strong reference cycles in specific scenarios. They are distinct from weak references in their fundamental guarantee: an unowned reference always expects its referenced object to be alive. This makes them non-optional and avoids the overhead of optional checking, but at the cost of a runtime crash if that guarantee is violated.
By carefully analyzing the lifetime relationships between your objects, you can judiciously choose between weak and unowned to ensure your applications are free of memory leaks and run efficiently and reliably. Remember: when in doubt, weak is often the safer default, as it gracefully handles deallocation by becoming nil. Only use unowned when you have a strong, provable guarantee of the referenced object's continued existence.
Happy Swifting!