Memory and ARC
How ARC frees objects, how retain cycles form and are broken with `weak`/`unowned`, and how you hunt a leak with the memory tools.
12 questions
JuniorTheoryVery commonWhat is ARC — when are retain/release calls inserted, and how does it differ from a tracing garbage collector?
What is ARC — when are retain/release calls inserted, and how does it differ from a tracing garbage collector?
ARC inserts retain/release at compile time to count an object's owners and frees it the moment that count hits zero. Unlike a tracing garbage collector there is no background scan, so freeing is deterministic.
Common mistakes
- ✗Believing ARC scans the heap at runtime like a garbage collector
- ✗Thinking retain/release are decided at runtime rather than inserted by the compiler
- ✗Assuming deallocation timing is nondeterministic under ARC
Follow-up questions
- →What happens to the retain count when a
weakreference is added? - →Why can ARC still leak memory despite counting every reference?
JuniorTheoryVery commonweak vs unowned — what does each do when the referent is deallocated, and when is unowned justified?
weak vs unowned — what does each do when the referent is deallocated, and when is unowned justified?
A weak reference is optional and auto-zeroes to nil when its referent is freed, so reading it stays safe. unowned is non-optional and not zeroed, so it crashes after the referent dies — use it only when the referent outlives it.
Common mistakes
- ✗Swapping the semantics — thinking
weaktraps andunownedis the safe one - ✗Believing
unownedis auto-zeroed likeweak - ✗Assuming either keyword contributes to the retain count
Follow-up questions
- →Why is an
unownedaccess after deallocation undefined rather than a cleannil? - →How does
weakdiffer fromunowned(unsafe)at runtime?
JuniorTheoryCommonWhen is deinit called, what is still valid inside it, and why must you not resurrect self?
When is deinit called, what is still valid inside it, and why must you not resurrect self?
deinit runs the instant an object's count reaches zero, just before its memory is freed. Inside it self and its stored properties are valid, so you can release resources. Storing self cannot cancel the dealloc.
Common mistakes
- ✗Thinking
deinitruns on a delay after the object becomes unreachable - ✗Believing stored properties are already
nilinsidedeinit - ✗Assuming you can cancel deallocation by re-storing
self
Follow-up questions
- →In what order do
deinitcalls run up a subclass–superclass chain? - →Why should
deinitavoid startingasyncwork that capturesself?
JuniorTheoryCommonWhat is a retain cycle? Give the classic parent–child example and say which reference must break it.
What is a retain cycle? Give the classic parent–child example and say which reference must break it.
A retain cycle is two objects holding strong references to each other, so neither count reaches zero and both leak. In a parent–child pair the child's back-reference to the parent must be weak to break it.
Common mistakes
- ✗Thinking ARC automatically detects and breaks reference cycles
- ✗Marking the wrong side of the pair
weak(the owner instead of the back-reference) - ✗Believing a cycle only leaks if it spans multiple threads
Follow-up questions
- →When would you choose
unownedoverweakfor the child's back-reference? - →How does a closure stored on the parent create the same cycle?
MiddleDebuggingCommonFind the leak behind memory that grows on every screen push and pop
Find the leak behind memory that grows on every screen push and pop
The graph shows the popped controller still alive: a strong arrow to a stored closure and a strong arrow back form a cycle, so it is never freed. Capture [weak self] and the controller deallocates on pop, ending the growth.
Common mistakes
- ✗Blaming steady growth on caches instead of checking the retain graph
- ✗Expecting
viewDidDisappearor a manual nil to release a cycled controller - ✗Marking every edge
unownedinstead of breaking one withweak
Follow-up questions
- →How do you tell a strong arrow from a
weakone in the graph? - →Why does the leaked controller often bring an entire view hierarchy with it?
MiddleTheoryCommonWhat lives on the stack versus the heap in Swift — do structs always live on the stack?
What lives on the stack versus the heap in Swift — do structs always live on the stack?
Value types default to stack or inline storage; classes live on the heap under ARC. But no — a struct captured by a closure or boxed in an existential is heap-allocated too. Storage follows escaping and size.
Common mistakes
- ✗Claiming structs are always stack-allocated with no exceptions
- ✗Thinking
letvsvardecides stack vs heap placement - ✗Assuming a captured struct stays on the stack
Follow-up questions
- →How does capturing a struct in an escaping closure change its storage?
- →Why does boxing a struct into an
anyexistential allocate?
MiddleDebuggingCommonA Timer with target: self keeps a view controller alive forever — fix it
A Timer with target: self keeps a view controller alive forever — fix it
A scheduled Timer retains its target, and the run loop retains the timer, so target: self keeps the controller alive forever. Fix it with invalidate() in viewDidDisappear, or the block API with [weak self].
Common mistakes
- ✗Marking the
Timerpropertyweak— it does not break the run loop's hold - ✗Believing
Timerdoes not retain its target - ✗Expecting ARC or memory pressure to release the controller anyway
Follow-up questions
- →Why does an invalidated timer finally release its target?
- →How does the block-based
TimerAPI let you avoid the strongtarget?
MiddleDebuggingCommonAn [unowned self] closure crashes after the screen is popped — fix it
An [unowned self] closure crashes after the screen is popped — fix it
The closure outlives self, so its unowned reference points at freed memory and the call crashes — unowned wrongly assumed self would outlive it. Capture [weak self] with guard let self, so a dead self is a safe no-op.
Common mistakes
- ✗Misreading the dangling-pointer crash as a retain cycle
- ✗Blaming the thread instead of the
unownedlifetime assumption - ✗Switching to a strong capture, which reintroduces the leak
Follow-up questions
- →When is
[unowned self]actually the correct choice for a closure? - →Why does
guard let selfmake a dead-selfcall a safe no-op?
JuniorTheoryOccasionalWhich tool proves a leak — Instruments' Leaks and Allocations, or Xcode's Memory Graph Debugger — and what does each miss?
Which tool proves a leak — Instruments' Leaks and Allocations, or Xcode's Memory Graph Debugger — and what does each miss?
Leaks flags blocks with no references left but misses objects a cycle still owns. Allocations shows memory growing over time but cannot name the culprit. The Memory Graph Debugger shows the live retain graph and pinpoints cycles.
Common mistakes
- ✗Treating Leaks, Allocations, and the graph as interchangeable
- ✗Expecting Leaks to catch a cycle whose objects you still reference
- ✗Thinking Allocations by itself identifies the leaking object
Follow-up questions
- →Why can a retain cycle show as steady growth in Allocations but nothing in Leaks?
- →What does the Memory Graph Debugger's purple
!badge indicate?
MiddleTheoryOccasionalDoes a weak var cost anything? Explain side tables and what a zeroing weak reference implies.
Does a weak var cost anything? Explain side tables and what a zeroing weak reference implies.
Yes, weak is not free: the object gets a side-table entry listing its weak referrers, costing a small allocation and an indirection. On dealloc the runtime walks that table and atomically nils every weak reference, so nothing dangles.
Common mistakes
- ✗Believing
weakcompiles to a plain pointer with zero overhead - ✗Thinking the weak bookkeeping lives in the variable, not a side table on the object
- ✗Assuming you must manually nil a
weakreference indeinit
Follow-up questions
- →Why does zeroing need to be atomic with respect to deallocation?
- →How does
unownedavoid the side-table cost, and what does it give up?
SeniorTheoryOccasionalWhat is an autorelease pool, and when does modern Swift still need an explicit autoreleasepool?
What is an autorelease pool, and when does modern Swift still need an explicit autoreleasepool?
An autorelease pool defers its objects' release until the pool drains; the run loop drains one pool per iteration. You still wrap a loop that spawns many temporary Cocoa objects in an explicit autoreleasepool to cap peak memory.
Common mistakes
- ✗Believing pure-Swift code can never benefit from
autoreleasepool - ✗Confusing an autorelease pool with a garbage collector
- ✗Thinking temporaries are always freed at end of statement without a pool
Follow-up questions
- →Why does a
forloop reading many images blow up without an explicit pool? - →When does the main run loop drain its autorelease pool?
SeniorPerformanceRareA batch image loop spikes to 2 GB before the OS kills it — how do autoreleasepool and downsampling bound peak memory?
A batch image loop spikes to 2 GB before the OS kills it — how do autoreleasepool and downsampling bound peak memory?
Each pass decodes a full image into a big autoreleased buffer freed only when the run loop drains, so peak is the sum of all passes. Wrap the body in autoreleasepool to free each buffer per pass, and downsample at load to decode thumbnail pixels.
Common mistakes
- ✗Thinking
autoreleasepoolonly reorders frees and cannot lower peak - ✗Believing more concurrency reduces peak memory here
- ✗Skipping downsampling and decoding full-resolution pixels
Follow-up questions
- →How does
CGImageSourceCreateThumbnailAtIndexavoid decoding the full image? - →Where in the loop must the
autoreleasepoolblock go to help?