Memory and ARC
Swift uses Automatic Reference Counting (ARC) — an automatic memory-management mechanism that tracks the number of references to each instance of a reference type (class, actor) and deallocates it when the count reaches zero. Unlike a tracing garbage collector (Java, Go), ARC is deterministic — deallocation occurs immediately when the last reference is released. The principal mechanisms: strong (default — keeps the object alive), weak (does not keep alive; becomes nil when the object is deallocated), unowned (does not keep alive; expected to outlive the reference). The principal pitfall: retain cycles — circular strong references that prevent deallocation. The conventional defences: capture lists in closures, weak references in delegate patterns, unowned references when lifetime is guaranteed. The combination — automatic deterministic counting, the strong/weak/unowned distinction, capture lists for closures, the deinit for cleanup — is the substance of Swift’s memory model for reference types.
Value types (struct, enum) are not reference-counted — they are allocated inline (on the stack or as part of their containing context).
ARC basics
ARC tracks references to class instances:
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is initialised")
}
deinit {
print("\(name) is deinitialised")
}
}
var p1: Person? = Person(name: "Alice") // count: 1
var p2 = p1 // count: 2
var p3 = p1 // count: 3
p1 = nil // count: 2
p2 = nil // count: 1
p3 = nil // count: 0 → "Alice is deinitialised"
When all references are gone, the object is deallocated and deinit runs.
The mechanism is deterministic — the deallocation point is predictable.
Strong references
The default — admitted without any keyword:
class Box {
var value: Int = 0
}
var b1 = Box() // strong reference; count: 1
let b2 = b1 // count: 2
The strong reference keeps the object alive. When all strong references are gone, the object is deallocated.
Retain cycles
A retain cycle — two or more objects strongly referencing each other:
class Apartment {
var tenant: Person? // strong (default)
deinit { print("Apartment deinitialised") }
}
class Person {
var apartment: Apartment? // strong (default)
deinit { print("Person deinitialised") }
}
var alice: Person? = Person()
var unit5: Apartment? = Apartment()
alice?.apartment = unit5 // alice → unit5
unit5?.tenant = alice // unit5 → alice (cycle!)
alice = nil // count of Person: 1 (still referenced by unit5)
unit5 = nil // count of Apartment: 1 (still referenced by Person)
// BOTH objects leak — neither deinit prints
The cycle prevents deallocation; the objects leak. The conventional defences are weak and unowned.
Weak references
A weak reference admits referring to an object without incrementing the count:
class Apartment {
weak var tenant: Person? // weak reference
deinit { print("Apartment deinitialised") }
}
class Person {
var apartment: Apartment?
deinit { print("Person deinitialised") }
}
var alice: Person? = Person()
var unit5: Apartment? = Apartment()
alice?.apartment = unit5
unit5?.tenant = alice // weak; doesn't increment Person's count
alice = nil // Person's count: 0 → deallocated
// unit5.tenant becomes nil automatically
unit5 = nil // Apartment's count: 0 → deallocated
The weak reference:
- Does not keep the object alive.
- Becomes
nilwhen the referenced object is deallocated. - Must be a
var(notlet). - Must be optional (must admit
nil).
The conventional uses are delegate patterns, parent-child relationships (the child is weak-referenced from the parent context), and observation.
Unowned references
An unowned reference admits referring to an object without incrementing the count and is expected to always be valid:
class Customer {
let name: String
var card: CreditCard?
init(name: String) { self.name = name }
deinit { print("Customer deinitialised") }
}
class CreditCard {
let number: String
unowned let customer: Customer // unowned — card outlives card-customer association
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card deinitialised") }
}
The unowned reference:
- Does not keep the object alive.
- Does not become nil — the reference is assumed to remain valid.
- Crashes if accessed after the referenced object is deallocated.
- Need not be optional.
The conventional uses are when the lifetime relationship is guaranteed — the referenced object will not be deallocated before the reference. The mechanism admits substantial performance over weak (no nil check) at the cost of some safety.
When to use which
| Reference | Use when |
|---|---|
| strong (default) | The reference should keep the object alive |
| weak | The reference should not keep alive AND may become nil |
| unowned | The reference should not keep alive AND is guaranteed to outlive |
The conventional Swift discipline:
- Default to strong — it’s correct in most cases.
- Use weak in delegate patterns and other “may outlive” relationships.
- Use unowned sparingly — when the lifetime guarantee is genuinely solid.
Closures and capture lists
Closures may capture references; this admits retain cycles:
class ViewController {
var onTap: (() -> Void)?
var counter = 0
func setupHandler() {
onTap = { // closure captures self strongly
self.counter += 1
}
}
deinit { print("ViewController deinit") }
}
let vc = ViewController()
vc.setupHandler()
// vc → onTap → self (vc) — cycle!
The conventional defence is capture lists:
func setupHandler() {
onTap = { [weak self] in
self?.counter += 1 // optional access
}
}
// Or unowned (when the handler can't outlive self):
func setupHandler() {
onTap = { [unowned self] in
self.counter += 1
}
}
The capture list [weak self] or [unowned self] admits explicit reference behaviour.
For multiple captures:
closure = { [weak self, unowned manager] in
self?.update(via: manager)
}
The conventional contemporary discipline:
[weak self]— the safer default for closures stored onself.[unowned self]— only when the closure’s lifetime is genuinely scoped toself.
deinit
The deinit runs automatically when an instance is deallocated:
class FileHandle {
let path: String
private let descriptor: Int32
init(path: String) throws {
self.path = path
self.descriptor = open(path, O_RDONLY)
guard descriptor >= 0 else { throw IOError.openFailed }
}
deinit {
close(descriptor) // cleanup runs automatically
}
}
The mechanism admits deterministic resource cleanup; conventional for files, network connections, locks, etc.
deinit cannot:
- Take parameters.
- Return a value.
- Be called explicitly.
It runs exactly once per instance, when the count reaches zero.
Capture by value vs reference
In closures:
- Reference types (classes) are captured by reference — modifications to the same object are visible.
- Value types are captured by value by default — but if the closure mutates the captured value, the closure captures by reference (sort of):
var counter = 0 // value type (Int)
let increment = { counter += 1 } // captures counter
increment()
increment()
print(counter) // 2 — closure mutates captured counter
The mechanism admits substantial closures-as-state patterns.
For explicit value-capture:
let n = 42
let f = { [n] in print(n) } // captures n by value
unowned(unsafe) and unowned(safe)
Two forms of unowned:
unowned(aliasunowned(safe)) — checks at runtime; crashes on access after deallocation.unowned(unsafe)— does not check; undefined behaviour on access after deallocation.
The unsafe form admits substantial performance; conventional only when the lifetime is provably guaranteed and performance is critical.
ARC and threads
ARC operations are atomic — increment/decrement of reference counts is thread-safe. The mechanism admits multi-threaded code without explicit synchronisation around ARC.
For uncontended counts, the implementation is substantially efficient; contended counts (multiple threads holding references to the same object) admit substantial overhead.
The conventional discipline favours value types for thread-shared data — value types do not require ARC and admit independent copies.
Memory leaks and tools
ARC eliminates most memory leaks but admits leaks via:
- Retain cycles — the principal source of leaks in Swift code.
- Long-lived caches — substantial intentional retention.
- Closures captured strongly without realising.
- Notification center observers not removed.
The conventional debugging tools:
- Xcode Memory Graph Debugger — visualises retained instances.
- Instruments (Allocations, Leaks) — runtime profiling.
weakchecks during code review — particularly around closure captures.
Common patterns
Delegate pattern with weak
protocol DataSourceDelegate: AnyObject { // class-only; admits weak
func dataSource(_ source: DataSource, didLoad data: [Item])
}
class DataSource {
weak var delegate: DataSourceDelegate? // weak — delegate may outlive
func load() {
// ...
delegate?.dataSource(self, didLoad: items)
}
}
class ViewController: DataSourceDelegate {
let source = DataSource()
init() {
source.delegate = self
}
func dataSource(_ source: DataSource, didLoad data: [Item]) { /* ... */ }
}
The AnyObject constraint admits weak. The conventional Swift delegate pattern.
Closure with weak self
class ViewController {
var loader: DataLoader = DataLoader()
func loadData() {
loader.fetch { [weak self] result in
guard let self else { return }
self.handleResult(result)
}
}
}
The [weak self] admits the controller deallocating while the request is in flight.
Closure with unowned
class Controller {
let timer: DispatchSourceTimer
init() {
timer = DispatchSource.makeTimerSource()
timer.setEventHandler { [unowned self] in
self.tick() // safe — timer is owned by self; can't outlive
}
}
}
The [unowned self] admits no nil-check overhead; conventional when the closure’s lifetime is scoped to self.
Cleanup with deinit
class Subscription {
private let cancelToken: AnyObject
init(token: AnyObject) {
self.cancelToken = token
}
deinit {
// Automatic cleanup
NotificationCenter.default.removeObserver(cancelToken)
}
}
Observer pattern
class EventBus {
private var listeners: [WeakRef<AnyObject>] = []
func subscribe(_ listener: AnyObject) {
listeners.append(WeakRef(value: listener))
}
}
struct WeakRef<T: AnyObject> {
weak var value: T?
}
The pattern admits substantial observer-pattern implementations without retain cycles.
Cache with weak values
class WeakCache<Key: Hashable, Value: AnyObject> {
private var cache: [Key: WeakRef<Value>] = [:]
func get(_ key: Key) -> Value? {
cache[key]?.value
}
func set(_ key: Key, _ value: Value) {
cache[key] = WeakRef(value: value)
}
}
The pattern admits caches that don’t prevent value deallocation.
isKnownUniquelyReferenced for COW
struct CowArray<T> {
private final class Storage {
var items: [T]
init(_ items: [T]) { self.items = items }
}
private var storage: Storage
init() { storage = Storage([]) }
mutating func append(_ item: T) {
if !isKnownUniquelyReferenced(&storage) {
storage = Storage(storage.items) // copy
}
storage.items.append(item)
}
}
The isKnownUniquelyReferenced admits implementing COW manually.
Avoiding retain cycle in DispatchQueue
class Service {
func processInBackground() {
DispatchQueue.global().async { [weak self] in
self?.process()
}
}
}
Combine subscription cleanup
import Combine
class ViewModel {
private var cancellables: Set<AnyCancellable> = []
func bind() {
publisher.sink { [weak self] value in
self?.update(with: value)
}
.store(in: &cancellables)
}
deinit {
// cancellables are automatically cancelled when the set is released
}
}
A note on the conventional discipline
The contemporary Swift memory advice:
- Trust ARC — manual memory management is not admitted.
- Use
weakfor delegate patterns. - Use
[weak self]in closures stored on instances. - Use
unownedonly when lifetime is guaranteed. - Use
finalon classes — admits ARC optimisations. - Prefer value types — no ARC overhead, no cycles possible.
- Use
deinitfor cleanup of non-Swift resources. - Profile with Instruments for memory issues.
- Use Xcode’s Memory Graph Debugger to find cycles.
- Use
AnyObjectconstraint for class-only protocols admittingweak.
The combination — Automatic Reference Counting, the strong/weak/unowned distinction, capture lists for closures, deinit for cleanup, the conventional discipline of value types as the default — is the substance of Swift’s memory model. The discipline produces deterministic, predictable memory behaviour with substantial protection against the conventional reference-cycle pitfalls.