Swift: Swift Memory Management and ARC Tutorial

ARC is Swift's memory manager -- it automatically tracks and manages reference counts for objects. Understand it, and your app won't silently eat all the memory.

1. What You'll Learn


2. An iOS Developer's Real Story

(1) Pain Point: The App Gets Slower Then Crashes

Bob discovered a bizarre problem while testing his chat app: after navigating in and out of the chat page 5 times, the app consumed 500MB of memory and crashed outright on the 6th time. Using Xcode's memory debugger, he found that each entry to the chat page increased memory by 80MB, but only 10MB was freed upon exit. The root cause: the chat page's Message objects and User objects held strong references to each other, forming a retain cycle. ARC considered "both sides need each other," so neither was released.

(2) The Weak Reference Solution

SWIFT
// Before fix: retain cycle
class Message {
    let text: String
    var sender: User?  // Strong reference
}
class User {
    let name: String
    var lastMessage: Message?  // Strong reference
}
// After fix: use weak reference on one side to break the cycle
class User {
    let name: String
    weak var lastMessage: Message?  // Weak reference
}

(3) Benefits: Stable and Controllable Memory Usage

Dimension Before Fix After Fix
10 page navigations 480MB → crash 60MB stable
Object deallocation 60% cannot free 100% deallocated normally
User experience Gets slower over time Smooth throughout
Maintenance cost Don't know where the leak is Clear reference relationships

3. How ARC Works

ARC (Automatic Reference Counting) automatically tracks how many variables reference each class instance. When the reference count reaches zero, the instance is automatically freed.

100%
graph LR
    A["Person instance"] --> B["Ref count = 0: deallocated"]
    A --> C["Ref count = 1: alive"]
    A --> D["Ref count = 2: alive"]
    B --> E["var alice: Person? = Person()"]
    C --> F["alice = nil -> count 0 -> deinit"]
    D --> G["var ref2 = alice -> count 2"]
    G --> H["ref2 = nil -> count 1"]
    H --> I["alice = nil -> count 0 -> deinit"]

(1) Reference Counting Rules

Operation Reference Count Change Description
Create instance 0 -> 1 let obj = MyClass()
Assign to a strong reference +1 let ref2 = obj
Set strong reference to nil -1 ref2 = nil
Go out of scope -1 Local variable auto-destroyed

(2) Observing Deallocation with deinit

▶ Example: Observing Reference Count Changes

SWIFT
// ============================================
// Observing ARC reference count and deinit timing
// ============================================
class Person {
    let name: String
    init(name: String) { self.name = name; print("\(name) initialized") }
    deinit { print("\(name) deallocated") }
}
var alice: Person? = Person(name: "Alice")  // count = 1
var ref2 = alice                              // count = 2
print("Before ref2 = nil")
ref2 = nil                                    // count = 1
print("After ref2 = nil")
print("Before alice = nil")
alice = nil                                   // count = 0 -> deinit
print("After alice = nil")

Output:

TEXT 📖 Display only
Alice initialized
Before ref2 = nil
After ref2 = nil
Before alice = nil
Alice deallocated
After alice = nil

4. The Retain Cycle Problem

When two objects hold strong references to each other, ARC cannot release them -- both have reference counts >= 1 at all times.

100%
graph TB
    A["Message\nref count: 1"] -->|"strong ref sender"| B["User\nref count: 1"]
    B -->|"strong ref lastMessage"| A
    C["External ref message = nil"] -.->|"but still not freed"| A
    D["External ref user = nil"] -.->|"but still not freed"| B
    style A fill:#ffcccc
    style B fill:#ffcccc

(1) Retain Cycles Between Class Instances

Reference Combination Can It Free? Description
A strong -> B, B strong -> A Leak Classic retain cycle
A strong -> B, B weak -> A Normal One side weak breaks the cycle
A strong -> B, B unowned -> A Normal One side unowned (B's lifetime <= A's)

▶ Example: Chat Message Retain Cycle

SWIFT
// ============================================
// Retain cycle demo: Message and User hold each other
// ============================================
class Message {
    let text: String
    var sender: User?
    init(text: String) { self.text = text; print("Message initialized: \(text)") }
    deinit { print("Message deallocated: \(text)") }
}
class User {
    let name: String
    var lastMessage: Message?
    init(name: String) { self.name = name; print("User initialized: \(name)") }
    deinit { print("User deallocated: \(name)") }
}
print("--- Create ---")
var user: User? = User(name: "Bob")
var msg: Message? = Message(text: "Hello")
user?.lastMessage = msg
msg?.sender = user
print("--- Set nil ---")
msg = nil
user = nil
print("--- Observe output ---")
print("Notice: deinit was NOT triggered -- objects leaked!")

Output:

TEXT 📖 Display only
--- Create ---
User initialized: Bob
Message initialized: Hello
--- Set nil ---
--- Observe output ---
Notice: deinit was NOT triggered -- objects leaked!

(2) Retain Cycles in Closures

Closures capturing external variables also create strong references. If a closure is held by an object, and the closure captures that same object, a cycle is formed.

▶ Example: Closure Retain Cycle

SWIFT
// ============================================
// Retain cycle caused by a closure
// ============================================
class NetworkManager {
    var onComplete: (() -> Void)?
    let url: String
    init(url: String) { self.url = url; print("Manager initialized") }
    deinit { print("Manager deallocated") }
    func start() {
        onComplete = {
            print("Request completed: \(self.url)")
        }
    }
}
print("--- Create ---")
var manager: NetworkManager? = NetworkManager(url: "https://api.example.com")
manager?.start()
print("--- Set nil ---")
manager = nil
print("--- Observe ---")
print("deinit NOT triggered -- closure captures self, causing a leak")

Output:

TEXT 📖 Display only
--- Create ---
Manager initialized
--- Set nil ---
--- Observe ---
deinit NOT triggered -- closure captures self, causing a leak

5. Weak and Unowned References

Both weak and unowned references do not increase the reference count, but their semantics differ slightly.

(1) The weak Keyword

A weak reference must be allowed to become nil at runtime -- so it must be declared as var and an optional type ?.

(2) The unowned Keyword

An unowned reference assumes the referenced object will not be freed before itself -- no optional type is needed, but using a freed object will crash.

Feature weak unowned
Increases reference count? No No
Must be optional? Must be Optional Non-optional
After object is freed Automatically set to nil Points to a dangling pointer
Safe to use? Safe (nil check) Only safe if object exists
Use case Unsure if the other is alive The other is guaranteed to be alive

▶ Example: Fixing Retain Cycle with weak

SWIFT
// ============================================
// Using weak to break the retain cycle between classes
// ============================================
class Message {
    let text: String
    weak var sender: User?  // weak breaks the cycle
    init(text: String) { self.text = text; print("Message initialized: \(text)") }
    deinit { print("Message deallocated: \(text)") }
}
class User {
    let name: String
    var lastMessage: Message?
    init(name: String) { self.name = name; print("User initialized: \(name)") }
    deinit { print("User deallocated: \(name)") }
}
print("--- After fix ---")
var user: User? = User(name: "Bob")
var msg: Message? = Message(text: "Hello")
user?.lastMessage = msg
msg?.sender = user
print("--- Set nil ---")
msg = nil    // Message freed
user = nil   // User freed
print("--- Deallocated normally ---")

Output:

TEXT 📖 Display only
--- After fix ---
User initialized: Bob
Message initialized: Hello
--- Set nil ---
Message deallocated: Hello
User deallocated: Bob
--- Deallocated normally ---

(3) Fixing with Closure Capture Lists

Use [weak self] or [unowned self] to declare how the closure captures variables at definition time.

▶ Example: Fixing Closure Retain Cycle with Capture List

SWIFT
// ============================================
// Capture list fixes the closure retain cycle
// ============================================
class NetworkManager {
    var onComplete: (() -> Void)?
    let url: String
    init(url: String) { self.url = url; print("Manager initialized") }
    deinit { print("Manager deallocated") }
    func start() {
        onComplete = { [weak self] in
            guard let self = self else { return }
            print("Request completed: \(self.url)")
        }
    }
}
print("--- After fix ---")
var manager: NetworkManager? = NetworkManager(url: "https://api.example.com")
manager?.start()
manager = nil
print("--- Manager deallocated ---")

Output:

TEXT 📖 Display only
--- After fix ---
Manager initialized
--- Manager deallocated ---
Manager deallocated

6. Full Example: Chat App Message Management

SWIFT
// ============================================
// Full example: Memory-safe chat message management
// Uses weak/unowned + capture lists
// ============================================
import Foundation
// 1. User model
class UserProfile {
    let id: Int
    let name: String
    weak var latestMessage: Message?  // weak breaks the cycle
    var messages: [Message] = []
    init(id: Int, name: String) {
        self.id = id
        self.name = name
        print("UserProfile \(name) initialized")
    }
    deinit { print("UserProfile \(name) deallocated") }
    // 2. Use capture list in closure
    func createAutoReply() -> (() -> String) {
        return { [weak self] in
            guard let self = self else { return "User is offline" }
            return "\(self.name): Auto-reply - I'll get back to you later"
        }
    }
}
// 3. Message model
class Message {
    let id: Int
    let text: String
    var sender: UserProfile?  // Sender
    init(id: Int, text: String, sender: UserProfile?) {
        self.id = id
        self.text = text
        self.sender = sender
        print("Message #\(id) initialized")
    }
    deinit { print("Message #\(id) deallocated") }
}
// 4. Usage example
print("=== Send message ===")
var alice: UserProfile? = UserProfile(id: 1, name: "Alice")
var msg1: Message? = Message(id: 101, text: "Hello!", sender: alice)
alice?.latestMessage = msg1
msg1?.sender = alice
let reply = alice?.createAutoReply()
print(reply?() ?? "")
print("\n=== Exit chat ===")
msg1 = nil
alice = nil
print("=== All objects deallocated ===")

Output:

TEXT 📖 Display only
=== Send message ===
UserProfile Alice initialized
Message #101 initialized
Alice: Auto-reply - I'll get back to you later

=== Exit chat ===
Message #101 deallocated
UserProfile Alice deallocated
=== All objects deallocated ===

❓ FAQ

Q How do I choose between weak and unowned?
A If the referenced object might be freed first (e.g., a view controller's delegate), use weak. If the referenced object is guaranteed to outlive the reference (e.g., parent references child in a parent-child relationship), use unowned. When in doubt, use weak.
Q When must I use a closure capture list?
A When a closure is strongly held by an object (stored in a property) and the closure accesses self internally, you must use [weak self] or [unowned self]. Temporary closures (like animation completions) don't need it.
Q Do structs also have retain cycles?
A No. Structs are value types and have no reference counting. Only classes (reference types) can create retain cycles and memory leaks.
Q How do I detect retain cycles in an app?
A Use Xcode's Memory Graph Debugger -- run the app, click the memory graph button, and leaked objects are marked in purple. You can also use Instruments' Leaks template.
Q Can I declare all properties as weak?
A No. weak means "don't own," and if all properties are weak, the object is freed immediately. There must be a chain of strong references from root objects (AppDelegate, Window) to all active objects.

📖 Summary


📝 Exercises

  1. Basic: Create two classes Apartment and Tenant. Tenant holds a reference to the rented apartment; Apartment holds a list of tenants. Use weak to avoid retain cycles and verify that deinit is triggered correctly.
  2. Intermediate: Write a TimerManager class that internally holds a Timer and uses a closure as the callback. Access self's properties and methods in the closure, using a capture list to avoid a retain cycle.
  3. Challenge: Simulate a common leak scenario -- initiating a network request in a view controller and updating the UI in the closure callback. Build the leaking version, then fix it. Use deinit print to verify the fix works.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏