Remember the days when iOS developers spent half their time counting retain
and release
calls? If you don't, consider yourself lucky. Today, let's talk about ARC (Automatic Reference Counting) – the technology that saved us from manual memory management hell and fundamentally changed how we write iOS apps.
The Dark Ages: Before ARC
Picture this: It's 2010. You're building an iOS app, and your code looks something like this:
- (void)doSomething {
NSString *myString = [[NSString alloc] initWithString:@"Hello"];
[self processString:myString];
[myString release]; // Must balance every alloc with release
// Forget this release? Memory leak.
// Extra release? Crash.
}
One missing release
? Memory leak. One extra release
? Crash. Fun times.
Manual Reference Counting (MRC) was like juggling chainsaws – technically possible, but one mistake and you're in trouble. Developers spent countless hours debugging retain cycles, analyzing static analyzer warnings, and arguing about whether autorelease
was a blessing or a curse.
Enter ARC: The Game Changer
Apple introduced ARC with iOS 5 and Xcode 4.2 in October 2011, and it wasn't just an incremental improvement – it was a paradigm shift. The pitch was simple: "What if the compiler could handle all those retain
and release
calls for you?"
But here's the thing most people miss: ARC isn't garbage collection. There's no runtime overhead, no garbage collector thread running in the background. It's still reference counting, just automated at compile time. The compiler analyzes your code and inserts the exact same retain
and release
calls you would have written manually (but without the mistakes).
Think of ARC as having a really meticulous colleague who follows you around, cleaning up your memory management code. Except this colleague never gets tired, never makes mistakes, and works at compile time so there's zero runtime cost.
How ARC Actually Works
Let's break this down with a real example:
func createUser() {
let user = User(name: "John") // ARC: retain count = 1
let sameUser = user // ARC: retain count = 2
processUser(user) // Passed to function, still retained
} // End of scope: ARC releases both references, retain count = 0, object deallocated
Here's what's happening under the hood:
- Object Creation: When you create an object, ARC gives it a retain count of 1
- Assignment: When you assign that object to another variable, retain count goes up
- Scope Exit: When variables go out of scope, retain count goes down
- Deallocation: When retain count hits zero, the object is immediately deallocated
Simple, right? Well... mostly.
Something That can Bite You
1. The Classic Retain Cycle
This is the big one. The career-ender. The bug that ships to production.
class ViewController: UIViewController {
var closure: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
// THIS CREATES A RETAIN CYCLE
closure = {
self.view.backgroundColor = .red
}
}
}
What's happening here? The view controller owns the closure (strong reference), and the closure captures self
(another strong reference). They're keeping each other alive forever. It's like two people in a pool, each holding the other's head above water – nobody drowns, but nobody can leave either.
The Fix:
closure = { [weak self] in
self?.view.backgroundColor = .red
}
That [weak self]
is your escape hatch. It tells the closure: "Hey, you can reference this view controller, but don't keep it alive just for your sake."
2. The Unowned Trap
"Unowned is just like weak but without the optional!" No. Stop. This thinking will hurt you.
class Child {
unowned let parent: Parent // I'm 100% sure parent will outlive child
func doSomething() {
parent.update() // If parent is gone, this crashes
}
}
Unowned is a promise to the compiler: "This reference will NEVER be nil when I use it." Break that promise, and your app crashes. Use unowned only when you have a parent-child relationship where the child literally cannot outlive the parent.
Real-world safe example:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = { [unowned self] in
// Safe because asHTML can't exist without HTMLElement
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
}
3. The Collection Confusion
class Node {
var children: [Node] = [] // Strong references to all children
weak var parent: Node? // Weak reference to parent
func addChild(_ child: Node) {
children.append(child)
child.parent = self // Correct: parent-child with no cycle
}
}
Collections (arrays, dictionaries, sets) hold strong references by default when those elements are reference types (classes). If you're building trees or graphs, you need to think carefully about which references should be weak to avoid cycles.
4. The NotificationCenter/Timer Trap
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// WRONG: Creates a retain cycle with Timer
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
self.updateUI() // Strong reference to self!
}
// RIGHT: Use weak self
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
self?.updateUI()
}
// Or even better in iOS 10+: No retain cycle!
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotification),
name: .someNotification,
object: nil
)
// This is automatically cleaned up when self is deallocated
}
}
Practical Tips to Avoid Memory Leaks
1. The Weak-Strong Dance Pattern
This is your bread and butter:
someAsyncOperation { [weak self] in
guard let self = self else { return }
// Use self everywhere in this closure - it's now strongly held
self.doThis()
self.doThat()
}
2. Use Instruments, Not Guesswork
Stop guessing where your retain cycles are. Fire up Instruments, use the Leaks tool, and look at the Memory Graph Debugger (that little arrow icon in Xcode's debug bar). These tools will show you exactly what's keeping what alive.
3. Think in Object Graphs
Before you write code, sketch out your object relationships. Ask yourself:
- Who owns whom?
- Can this create a cycle?
- What should happen when the user navigates away?
4. Delegates Should Almost Always Be Weak
protocol MyDelegate: AnyObject { // AnyObject = class-only protocol
func didSomething()
}
class MyClass {
weak var delegate: MyDelegate? // WEAK!
}
5. Know Your Closure Context
Not every closure needs [weak self]
. If a closure is non-escaping or you want to keep self alive until completion, strong references are fine:
// Non-escaping closure - no need for weak self
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0 // This is fine!
}
// Array operations - non-escaping
let names = users.map { user in
return self.formatName(for: user) // Also fine!
}
// But be careful with stored closures or async operations
networkManager.onCompletion = { [weak self] result in
self?.handleResult(result) // This needs weak!
}
Quick Reference: When to Use What
Use weak
when:
- Creating delegates
- Self is captured in a closure that self owns
- You're unsure about object lifetimes
- Breaking retain cycles
Use unowned
when:
- You're 100% certain the reference will never be nil
- Classic example: A closure that can't outlive its owner
- You've profiled and need that tiny performance gain
Use strong (default) when:
- You want to keep something alive
- Parent owning children
- Temporary references in functions
- Non-escaping closures
The Modern ARC Mindset
Here's the thing: ARC isn't something you fight against or work around. It's a tool that, when understood, makes your code both safer and cleaner. The key is shifting your mental model from "managing memory" to "managing relationships."
Every strong reference is a relationship that says "I need you to exist." Every weak reference says "I'd like to talk to you if you're around." Every unowned reference says "You better be there when I call."
Common Misconceptions about ARC
"ARC means no memory leaks" - Wrong. ARC prevents you from forgetting to release objects, but retain cycles can still cause leaks.
"Weak references are slow" - Negligible in 99% of cases. They use a side table lookup, but unless you're accessing them thousands of times per second, you won't notice.
"Unowned is dangerous, never use it" - It has its place. When used correctly, it's perfectly safe.
Top comments (1)
ARC is reference counting, just automated at compile time.