Answer
The core idea
ARC does not use one reference count. Conceptually every class instance has three:
- strong count: ownership. When it hits zero,
deinitruns. - unowned count: non-owning direct references that still need the allocation to stay valid for a while.
- weak count: entries that must be able to observe “this object is gone.”
The short interview answer is: strong owns the object, unowned points at the object without owning it, and weak points at a side table so it can become nil after the object dies.
1. Strong: ownership in the object header
A strong reference is the default. The variable holds a pointer to the object, and forming it increments the strong count.
class Person {
let name: String
init(name: String) { self.name = name }
}
var owner: Person? = Person(name: "Alex") // strong count == 1
var alias = owner // strong count == 2
owner = nil // strong count == 1
alias = nil // strong count == 0 → deinitUnder the hood, early in an object's life those counts often live inline in the object header next to the type metadata. Strong retain and release are the fast path: bump a counter on the object itself.
2. Unowned: same pointer shape, different count
An unowned reference also stores a pointer to the object, not through a side table. It does not keep the strong count above zero, so it cannot create a retain cycle by itself.
final class CreditCard {
unowned let customer: Customer
init(customer: Customer) {
self.customer = customer
}
}
final class Customer {
var card: CreditCard?
}What changes under the hood:
- forming
unownedbumps the unowned count - the pointer still aims at the
HeapObject - when the strong count hits zero,
deinitstill runs - the raw allocation is only freed once the unowned count reaches zero as well
That staged teardown is why unowned can be cheaper than weak: no side-table indirection on the happy path. The tradeoff is safety. After deinit, using the unowned reference is invalid and traps (the usual “safe unowned” behavior). The language therefore requires a lifetime guarantee: the referenced object must outlive every use of the unowned pointer.
3. Weak: side table indirection
A weak reference must become nil when the object is gone. The object cannot answer that question after its memory is reclaimed, so Swift introduces a side table.
final class View {
weak var model: Model?
}Rough runtime shape:
- the first weak reference causes the runtime to allocate a side table for that object (a one-way upgrade)
- the weak variable points at the side table, not at the object
- the side table holds a pointer back to the object while it is alive, plus the refcount bookkeeping
- strong and unowned variables still point at the object
- when the strong count hits zero and deinit finishes, the side table's object pointer is cleared
- loading the weak reference then yields
nil - the side table itself is freed only after the weak count drains
So weak is optional in the type system because the runtime needs a place that can outlive the object and report emptiness. That extra hop is also why weak is slower than strong or unowned on hot paths.
4. How the three teardown stages fit together
A useful mental model of the cascade:
- strong count → 0: run
deinit, weak loads start returningnil, unowned loads become invalid - unowned count → 0: free the object's allocation
- weak count → 0: free the side table
You do not need every bit of the packing scheme in an interview. You do need the consequence: weak can nil out because it never relied on the object's memory staying around, and unowned cannot.
5. Why the language rules look the way they do
| Kind | Points at | Keeps object alive? | After deinit |
|---|---|---|---|
strong | object | yes | reference is gone or must be cleared by the owner |
unowned | object | no | must not be used, or the process traps |
weak | side table | no | reads as nil |
That is why API guidance falls out of the runtime:
- use
weakwhen the other object may disappear first (delegates, parent pointers, many closure captures) - use
unownedonly when lifetime is guaranteed and you want to avoid optionals and side-table cost - leave
strongas the default ownership edge
Retain cycles are what happen when strong edges form a loop. Breaking a cycle with weak or unowned is the design move. The under-the-hood difference above is why those two non-owning options are not interchangeable.
Interview angle
Lead with the three counts: strong owns, unowned is a direct non-owning pointer, weak is a side-table pointer that can become nil. Explain that weak needs the side table so the object can be destroyed while references still exist, and that unowned skips that indirection at the cost of a lifetime guarantee. Close with the teardown order (deinit, then free object, then free side table) and the Swift book chapter on Automatic Reference Counting.