Swift: Swiftメモリ管理とARCチュートリアル:自動参照カウントと循環参照の修正
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
- How ARC (Automatic Reference Counting) works
- The core differences between strong, weak, and unowned references
- Causes and common scenarios of retain cycles
- Fixing retain cycles with closure capture lists
- Detecting memory leaks with Instruments
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
// 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.
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
// ============================================
// 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 📖 参照専用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.
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
// ============================================
// 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 📖 参照専用--- 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
// ============================================
// 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 📖 参照専用--- 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
// ============================================
// 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 📖 参照専用--- 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
// ============================================
// 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 📖 参照専用--- After fix --- Manager initialized --- Manager deallocated --- Manager deallocated
6. Full Example: Chat App Message Management
// ============================================
// 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 📖 参照専用=== 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
[weak self] or [unowned self]. Temporary closures (like animation completions) don't need it.📖 Summary
- ARC automatically manages class instance memory; instances are freed when the reference count reaches zero
- A retain cycle occurs when two objects hold strong references to each other, preventing either from being freed
- Weak references don't increase the reference count and are automatically set to nil when the object is freed
- Unowned references don't increase the reference count and don't become nil -- accessing a freed object causes a crash
- Closure capture lists
[weak self]break retain cycles caused by closures - Use Xcode's Memory Graph Debugger to visually detect retain cycles
📝 Exercises
- Basic: Create two classes
ApartmentandTenant. 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. - Intermediate: Write a
TimerManagerclass that internally holds aTimerand uses a closure as the callback. Access self's properties and methods in the closure, using a capture list to avoid a retain cycle. - 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.