ARC Fundamentals
How ARC Works
Automatic Reference Counting (ARC) is Swift memory management system. Every time a new reference to an object is created, its reference count increments. When a reference goes out of scope or is set to nil, the count decrements. When the count reaches zero, the object is deallocated.
Strong References
By default, all references in Swift are strong:
class User {
let name: String
init(name: String) { self.name = name }
deinit { print("\(name) deallocated") }
}
var user1: User? = User(name: "Alice") // count = 1
var user2 = user1 // count = 2
user1 = nil // count = 1
user2 = nil // count = 0, deallocated
Weak References
Weak references do not increase the reference count and are automatically set to nil when the object is deallocated:
class Apartment {
let unit: String
weak var tenant: User? // Does not retain User
init(unit: String) { self.unit = unit }
}
var alice: User? = User(name: "Alice")
var apt = Apartment(unit: "4A")
apt.tenant = alice // Does not increase Alice reference count
alice = nil // Alice is deallocated, apt.tenant becomes nil
Weak references must be declared as Optional since they can become nil.
Unowned References
Unowned references also do not increase the reference count but are not set to nil. Use them when the referenced object will never be nil during the reference lifetime:
class Customer {
let name: String
var card: CreditCard?
init(name: String) { self.name = name }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
}
Unowned is faster than weak but will crash if the object is deallocated while the reference exists.
Retain Cycles & Weak References
Common Retain Cycles
Retain cycles occur when two objects hold strong references to each other:
class ViewController: UIViewController {
var onComplete: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onComplete = {
self.dismiss(animated: true) // self is strongly captured
}
}
}
Breaking Retain Cycles
// Solution 1: Capture list with weak self
onComplete = { [weak self] in
self?.dismiss(animated: true)
}
// Solution 2: Capture list with unowned self
onComplete = { [unowned self] in
self.dismiss(animated: true)
}
Delegate Retain Cycles
Delegates should always be weak to prevent retain cycles:
protocol DataManagerDelegate: AnyObject {
func didUpdate(_ manager: DataManager)
}
class DataManager {
weak var delegate: DataManagerDelegate?
func update() { delegate?.didUpdate(self) }
}
NotificationCenter Retain Cycles
Observers can cause retain cycles. Use block-based observer with weak self:
class SettingsViewModel {
private var observer: NSObjectProtocol?
func setupObserver() {
observer = NotificationCenter.default.addObserver(
forName: .settingsChanged, object: nil, queue: .main
) { [weak self] _ in
self?.handleChange()
}
}
deinit {
if let observer = observer {
NotificationCenter.default.removeObserver(observer)
}
}
}
Memory Debugging
Memory Graph Debugger
Xcode built-in Memory Graph Debugger (Cmd+Shift+M) shows a visual graph of all live objects. Red indicators show likely retain cycles. Click on an object to see its reference graph and identify strong reference loops.
Using Instruments for Memory
The Allocations instrument tracks every allocation. Sort by Total Bytes to find the largest allocations. Check Persistent count for objects that should be deallocated. Use Mark Generation to compare memory between time points.
Common Memory Pitfalls
// Caching without eviction
class ImageCache {
var cache: [String: UIImage] = [:] // Grows forever
func load(url: String) -> UIImage {
if let cached = cache[url] { return cached }
let image = downloadImage(url)
cache[url] = image
return image
}
}
// Fix with NSCache or size limit
class ImageCache {
private let cache = NSCache<NSString, UIImage>()
func load(url: String) -> UIImage {
if let cached = cache.object(forKey: url as NSString) {
return cached
}
let image = downloadImage(url)
cache.setObject(image, forKey: url as NSString)
return image
}
}
Debugging Use-After-Free
Use the Zombies instrument to detect accessing deallocated objects. Enable it in Instruments and run your app. The instrument flags every message sent to a deallocated object, showing where it was allocated and where it was freed.
Memory Best Practices
- Use value types (struct, enum) when possible to avoid reference counting overhead
- Prefer weak over unowned unless you are certain the reference will not outlive the object
- Release large data structures when they are no longer needed
- Use NSCache for image and data caching with automatic eviction
- Profile with the Memory Graph Debugger regularly during development
Quiz
1. What happens when an object reference count reaches zero in ARC?
2. When should you use weak vs unowned references?
3. What causes a retain cycle in closures?
4. How do you access the Memory Graph Debugger?
Flashcards
Question
What is the difference between weak and unowned?
Click to reveal answer
Answer
Weak references become nil when the object is deallocated and must be Optional. Unowned references do not become nil and will crash if accessed after deallocation.
Question
What is a retain cycle?
Click to reveal answer
Answer
When two objects hold strong references to each other, preventing either from being deallocated. Broken with weak or unowned references.
Question
Why use NSCache instead of a Dictionary for caching?
Click to reveal answer
Answer
NSCache automatically evicts objects under memory pressure. Dictionaries grow unbounded and can cause memory warnings.
Revision Notes
Key Takeaways
- 1. ARC manages memory through reference counting
- 2. Retain cycles prevent deallocation - use weak/unowned to break them
- 3. Weak references must be Optional type
- 4. Use Memory Graph Debugger to visualize object references
- 5. Prefer value types to avoid reference counting overhead
Interview Tips
- • Explain the difference between strong, weak, and unowned
- • Describe how ARC works under the hood
- • Walk through finding and fixing a retain cycle
- • Discuss when to use NSCache vs Dictionary for caching
Cheat Sheet
Memory Management Quick Reference
- Strong (default): increases reference count
- Weak: does not increase count, becomes nil on dealloc
- Unowned: does not increase count, crashes if accessed after dealloc
- Use [weak self] in closures to prevent retain cycles
- Delegates should always be weak
- NSCache for automatic memory eviction