Subscribe to Jacob’s Tech Tavern for free to get ludicrously in-depth articles on iOS, Swift, tech, & indie projects in your inbox every two weeks.
Paid subscribers unlock Quick Hacks, my advanced tips series, and enjoy exclusive early access to my long-form articles.
Every time you use a class, closure, or actor, Swift is storing information on the heap. The variable you’re passing around is really a pointer to this address in memory (a.k.a. a reference).
Back in the olden days, Mac and iOS developers would need to manage this memory manually through functions like alloc()
to create a block of memory on the heap, retain()
to add another reference, and release()
to free up the memory.
In 2011, Apple introduced ARC — automatic reference counting — which made the compiler write this boilerplate memory management code itself, saving untold centuries of cumulative dev-time.
ARC introduced the concept of different types of reference.
Strong references
This is the default reference; a pointer which makes sure the memory it points at remains alive while the pointer is in use.
Creating a strong reference increments a heap object’s reference count (a.k.a. refCount), and the object is deallocated, freeing up the memory in the heap, when refCount hits zero.
Weak references
weak
references allow heap objects to point at each other without making a retain cycle. Developers can hence create references to classes in closures and delegates without keeping them alive unnecessarily.
weak
references point at the heap object in memory without incrementing refCount. If refCount dips to zero, the object is deallocated and the weak
reference becomes nil
— this prevents memory leaks in closures.
To handle this modus operani, weak
references always come wrapped in an optional.
// StoreViewModel.swift
func loadStorefront() {
api.fetchInventory() { [weak self] inventory in
self?.inventory = inventory
}
}
In this example, inventory is fetched over the network, and the closure callback runs when data arrives. self
is weak
ly captured, meaning that the properties on StoreViewModel
can be updated in the closure. If the user leaves the screen, and the view model’s strong refCount becomes zero, then the runtime can deinitialize the view model — it won’t be kept alive in memory while waiting for the callback, and so nothing happens when inventory is returned.
Unowned references
A third type of reference was introduced with Swift to help prevent retain cycles: unowned
. This behaves similarly to a weak
reference, except it assumes that the memory of the referenced object will always outlive the unowned
pointer referencing it.
// StoreSingleton.swift
static let sharedInstance = StoreSingleton()
private init() {}
func configureStorefront() {
api.fetchInventory() { [unowned self] inventory in
self.inventory = inventory
}
}
Here, we have a “true singleton” (with a private init
), so we can be sure the lifetime of the object will outlive any closures. The [unowned self]
in the closure capture list prevents a retain cycle from being created (though, for a singleton, we don’t actually need to worry about retain cycles).
The closure is a little bit simpler, and unowned
comes with a few performance benefits compared to weak
references:
There is no need to use optional chaining or unwrapping operations.
unowned
references store less metadata as they don’t create a side table on the heap object.Accessing the memory pointed at by
weak
references involves one extra jump, or layer of indirection, between pointers.
This performance comes with a catch though: if you get the lifetimes wrong, and the closure or property outlives the unowned
reference, your app will crash.
There is actually also a 4th type, the
unowned(unsafe)
reference, which disables runtime safety checks. Instead of crashing if the referenced object is deallocated, the program will happily accessing the memory address anyway. If crashes are the second-worst thing you do to your users, corrupting user data is #1.
Is unowned worth it?
Obviously, crashes are pretty bad. And premature optimisation, as Knuth is frequently misquoted, is a temptation about which we should be mindful. So we shouldn’t use unowned
references willy-nilly just to get a little bit more performance.
If you’re reading my blog, you probably know what you are doing when it comes to Swift. But can you be certain this is true for everybody on your team?
Can you be sure every code review you perform at 5pm on a Friday is judicious about understanding the many-branching code paths that lead to object deinstantiation, every time you spot the unowned
keyword?
Ultimately, unowned
isn’t a programming problem. It’s a human problem.
Performance costs of weak references
We understand the risks of unowned
. Let’s talk about the benefits compared to weak
references.
It’s all about performance.
I’m not going to pretend the handful of CPU instructions involved in optional unwrapping the weak
reference is important. Optionals are secretly enums; value types which live on the stack. Overhead from manipulating these is negligible, as they eschew the need for thread-safe, locked access to the heap.
Let’s talk about memory. weak
ly referencing a heap object for the first time creates a side table, a lightweight piece of metadata stored outside the heap object’s memory layout. Side tables contain a pointer back to the heap object, the weak
reference count; and refCounts for any integer-overflowed strong & unowned
references. unowned
references do not themselves require a side table.
If this sounds like a tiny handful of bits, you’d be right. It’s an extremely small amount of additional memory. So small in fact, that the runtime doesn’t require extra work on deinit to pre-emptively nil-out weak
references. Until the weak
reference is discarded, the lightweight side table remains in memory and checks the lifecycle state of the heap object to return either nil
or the object.
There is some overhead here. weak
references point at this side table, which in turn points to the actual heap object. This indirection adds a tiny bit more overhead to weak
references, and runs a small risk of expensive CPU cache misses.
unowned
references, in contrast, point directly to the heap object in memory. There is less indirection, however the runtime still needs to check the heap object’s lifecycle state — so it knows whether to return the object or crash out with swift_abortRetainUnowned
.
If you found this section terribly interesting, take a look at the Swift runtime source code. Much of this information is explained in the comments on runtime/HeapObject.cpp and shims/RefCount.h.
In the Swift source code, I actaully bumped into my old friend,
isUniquelyReferenced
— check out COW2LLVM for a serious deep-dive, where we end up deep down in of the bit-level memory layout ofRefCount
and side tables.
When it makes sense to use unowned
From looking at the Swift source and understanding the implementation of weak
and unowned
references, it is clear that the benefits of using unowned
are slim, and the risk of causing a fatal error is often not worth this performance benefit.
But, everything in computer science is about trade-offs, and there are some instances where this trade-off makes sense.
When you have a large number of references, and runtime performance becomes your critical bottleneck, for example this developer making a game engine with many on-screen objects.
When you have a vast number of references, and memory is so constrained that saving a handful of bytes becomes a relevant concen — perhaps if you want to optimise some part of the code to fit into an L1 CPU cache.
When you’re an indie developer not relying on any other contributors, you know exactly what you’re doing, and have your eyes open about eating the cost of crashes in production.
I hope you’ll agree, however, that using [unowned self]
in a single closure callback is an pretty poor trade-off for a few clock cycles and, at worst, a few nanoseconds of cache miss.
Conclusion
unowned
references help you to prevent retain cycles in ARC. They offer a Faustian bargain: a sliver of runtime performance in exchange for the risk of crashing if you get it wrong.
There are some instances, such as with huge numbers of references in critically bottlenecked systems, in which they make sense to use. But [unowned self]
in a single closure capture list is not one of them.
I recommend you just stick with [weak self]
and treat unowned
as an exceptional case — that is, a case with which you’re willing to swallow an exception if you get it wrong.
Long story short — if you’re confident enough to use unowned
in your code, just use unowned(unsafe)
, you coward*. It’s marginally more performant.
*p.s. Don’t do this.