Swift: Swift 内存管理与 ARC 教程:自动引用计数与循环引用修复
ARC 是 Swift 的内存管家——自动跟踪和管理对象的引用计数。理解它,你的 App 才不会悄悄吃掉所有内存。
1. 你将学到
- ARC 自动引用计数的基本工作原理
- 强引用、弱引用和无主引用的核心区别
- 循环引用的产生原因和常见场景
- 闭包捕获列表的循环引用修复方法
- 如何用 Instruments 工具检测内存泄漏
2. 一个 iOS 开发者的真实故事
(1) 痛点:App 越用越卡最后闪退
Bob 在测试他开发的聊天 App 时发现一个诡异问题:在聊天页面反复进出 5 次后,App 占用了 500MB 内存,第 6 次直接闪退。用 Xcode 的内存调试器一看——每次进入聊天页面,内存都涨 80MB,退出后只释放 10MB。
根本原因:聊天页面的 Message 对象和 User 对象互相持有强引用,形成循环引用。ARC 认为"双方互相需要",所以谁都不释放。
(2) 弱引用的解法
SWIFT
// 修复前:循环引用
class Message {
let text: String
var sender: User? // 强引用
}
class User {
let name: String
var lastMessage: Message? // 强引用
}
// 修复后:一端用弱引用打破循环
class User {
let name: String
weak var lastMessage: Message? // 弱引用
}
(3) 收益:内存开销稳定可控
| 维度 | 修复前 | 修复后 |
|---|---|---|
| 10 次页面跳转 | 480MB → 闪退 | 60MB 稳定 |
| 对象释放 | 60% 无法释放 | 100% 正常释放 |
| 用户体验 | 越用越卡 | 全程流畅 |
| 维护成本 | 不知道哪泄漏 | 清晰的引用关系 |
3. ARC 工作原理
ARC(Automatic Reference Counting)自动跟踪每个类实例被多少个变量引用。引用计数归零时,实例自动释放。
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) 引用计数规则
| 操作 | 引用计数变化 | 说明 |
|---|---|---|
| 创建实例 | 0 → 1 | let obj = MyClass() |
| 赋值给强引用 | +1 | let ref2 = obj |
| 强引用置 nil | -1 | ref2 = nil |
| 超出作用域 | -1 | 局部变量自动销毁 |
(2) deinit 观察释放时机
▶ 示例:观察引用计数的变化
SWIFT
// ============================================
// 观察 ARC 引用计数与 deinit 时机
// ============================================
class Person {
let name: String
init(name: String) { self.name = name; print("\(name) 初始化") }
deinit { print("\(name) 释放") }
}
var alice: Person? = Person(name: "Alice") // count = 1
var ref2 = alice // count = 2
print("ref2 = nil 前")
ref2 = nil // count = 1
print("ref2 = nil 后")
print("alice = nil 前")
alice = nil // count = 0 → deinit
print("alice = nil 后")
输出:
TEXT 📖 仅展示Alice 初始化 ref2 = nil 前 ref2 = nil 后 alice = nil 前 Alice 释放 alice = nil 后
4. 循环引用问题
当两个对象互相持有强引用时,ARC 无法释放它们——双方引用计数始终 >= 1。
graph TB
A["Message\nref count: 1"] -->|"强引用 sender"| B["User\nref count: 1"]
B -->|"强引用 lastMessage"| A
C["外部引用 message = nil"] -.->|"但仍无法释放"| A
D["外部引用 user = nil"] -.->|"但仍无法释放"| B
style A fill:#ffcccc
style B fill:#ffcccc
(1) 类实例间的循环引用
| 引用组合 | 能否释放 | 说明 |
|---|---|---|
| A 强 → B,B 强 → A | ❌ 泄漏 | 经典循环引用 |
| A 强 → B,B 弱 → A | ✅ 正常 | 一端弱引用打破循环 |
| A 强 → B,B 无主 → A | ✅ 正常 | 一端无主引用(B 生命周期不大于 A) |
▶ 示例:聊天消息循环引用
SWIFT
// ============================================
// 循环引用演示:消息与用户互相持有
// ============================================
class Message {
let text: String
var sender: User?
init(text: String) { self.text = text; print("消息初始化: \(text)") }
deinit { print("消息释放: \(text)") }
}
class User {
let name: String
var lastMessage: Message?
init(name: String) { self.name = name; print("用户初始化: \(name)") }
deinit { print("用户释放: \(name)") }
}
print("--- 创建 ---")
var user: User? = User(name: "Bob")
var msg: Message? = Message(text: "你好")
user?.lastMessage = msg
msg?.sender = user
print("--- 置 nil ---")
msg = nil
user = nil
print("--- 观察输出 ---")
print("注意:deinit 没有触发,对象泄漏了!")
输出:
TEXT 📖 仅展示--- 创建 --- 用户初始化: Bob 消息初始化: 你好 --- 置 nil --- --- 观察输出 --- 注意:deinit 没有触发,对象泄漏了!
(2) 闭包中的循环引用
闭包捕获外部变量时也会产生强引用。如果闭包被对象持有,同时闭包又捕获了该对象,就形成了循环。
▶ 示例:闭包循环引用
SWIFT
// ============================================
// 闭包导致的循环引用
// ============================================
class NetworkManager {
var onComplete: (() -> Void)?
let url: String
init(url: String) { self.url = url; print("Manager 初始化") }
deinit { print("Manager 释放") }
func start() {
onComplete = {
print("请求完成: \(self.url)")
}
}
}
print("--- 创建 ---")
var manager: NetworkManager? = NetworkManager(url: "https://api.example.com")
manager?.start()
print("--- 置 nil ---")
manager = nil
print("--- 观察 ---")
print("deinit 未触发,闭包捕获 self 导致泄漏")
输出:
TEXT 📖 仅展示--- 创建 --- Manager 初始化 --- 置 nil --- --- 观察 --- deinit 未触发,闭包捕获 self 导致泄漏
5. 弱引用与无主引用
弱引用(weak)和无主引用(unowned)都不增加引用计数,但语义略有不同。
(1) weak 关键字
弱引用必须在运行时可能变为 nil——所以必须声明为 var 和可选类型 ?。
(2) unowned 关键字
无主引用假定引用的对象不会比自身先释放——不需要可选类型,但使用已释放对象会崩溃。
| 特性 | weak | unowned |
|---|---|---|
| 是否增加引用计数 | ❌ 否 | ❌ 否 |
| 是否可选 | ✅ 必须是 Optional | ❌ 非 optional |
| 对象释放后 | 自动置 nil | 指向悬垂指针 |
| 安全使用 | ✅ 安全(nil 检查) | ⚠️ 对象存在才能用 |
| 适用场景 | 不确定对方是否存活 | 对方一定存活 |
▶ 示例:用 weak 修复循环引用
SWIFT
// ============================================
// 使用 weak 打破类之间的循环引用
// ============================================
class Message {
let text: String
weak var sender: User? // weak 打破循环
init(text: String) { self.text = text; print("消息初始化: \(text)") }
deinit { print("消息释放: \(text)") }
}
class User {
let name: String
var lastMessage: Message?
init(name: String) { self.name = name; print("用户初始化: \(name)") }
deinit { print("用户释放: \(name)") }
}
print("--- 修复后 ---")
var user: User? = User(name: "Bob")
var msg: Message? = Message(text: "你好")
user?.lastMessage = msg
msg?.sender = user
print("--- 置 nil ---")
msg = nil // Message 释放 ✓
user = nil // User 释放 ✓
print("--- 正常释放 ---")
输出:
TEXT 📖 仅展示--- 修复后 --- 用户初始化: Bob 消息初始化: 你好 --- 置 nil --- 消息释放: 你好 用户释放: Bob --- 正常释放 ---
(3) 闭包捕获列表修复
用 [weak self] 或 [unowned self] 在闭包定义时声明捕获方式。
▶ 示例:用捕获列表修复闭包循环
SWIFT
// ============================================
// 捕获列表修复闭包循环引用
// ============================================
class NetworkManager {
var onComplete: (() -> Void)?
let url: String
init(url: String) { self.url = url; print("Manager 初始化") }
deinit { print("Manager 释放") }
func start() {
onComplete = { [weak self] in
guard let self = self else { return }
print("请求完成: \(self.url)")
}
}
}
print("--- 修复后 ---")
var manager: NetworkManager? = NetworkManager(url: "https://api.example.com")
manager?.start()
manager = nil
print("--- Manager 已释放 ---")
输出:
TEXT 📖 仅展示--- 修复后 --- Manager 初始化 --- Manager 已释放 --- Manager 释放
6. 完整示例:聊天 App 消息管理
SWIFT
// ============================================
// 完整示例:内存安全的聊天消息管理
// 运用 weak/unowned + 捕获列表
// ============================================
import Foundation
// 1. 用户模型
class UserProfile {
let id: Int
let name: String
weak var latestMessage: Message? // weak 打破循环
var messages: [Message] = []
init(id: Int, name: String) {
self.id = id
self.name = name
print("UserProfile \(name) 初始化")
}
deinit { print("UserProfile \(name) 释放") }
// 2. 闭包中使用捕获列表
func createAutoReply() -> (() -> String) {
return { [weak self] in
guard let self = self else { return "用户已离线" }
return "\(self.name): 自动回复 - 稍后联系"
}
}
}
// 3. 消息模型
class Message {
let id: Int
let text: String
var sender: UserProfile? // 发送者
init(id: Int, text: String, sender: UserProfile?) {
self.id = id
self.text = text
self.sender = sender
print("Message #\(id) 初始化")
}
deinit { print("Message #\(id) 释放") }
}
// 4. 使用示例
print("=== 发送消息 ===")
var alice: UserProfile? = UserProfile(id: 1, name: "Alice")
var msg1: Message? = Message(id: 101, text: "你好!", sender: alice)
alice?.latestMessage = msg1
msg1?.sender = alice
let reply = alice?.createAutoReply()
print(reply?() ?? "")
print("\n=== 退出聊天 ===")
msg1 = nil
alice = nil
print("=== 所有对象已释放 ===")
输出:
TEXT 📖 仅展示=== 发送消息 === UserProfile Alice 初始化 Message #101 初始化 Alice: 自动回复 - 稍后联系 === 退出聊天 === Message #101 释放 UserProfile Alice 释放 === 所有对象已释放 ===
❓ 常见问题
Q weak 和 unowned 怎么选?
A 如果引用对象可能先被释放(如视图控制器的 delegate),用 weak。如果引用对象一定比自身存活更久(如父子关系中父引用子),用 unowned。拿不准时用 weak。
Q 闭包捕获列表什么时候必须写?
A 当闭包被对象强持有(存储在属性中),且闭包内部访问了 self 时,必须用
[weak self] 或 [unowned self]。临时闭包(如动画 completion)不需要。Q 结构体也有循环引用吗?
A 没有。结构体是值类型,不存在引用计数。只有类(引用类型)才会产生循环引用和内存泄漏。
Q 如何检测 App 中的循环引用?
A 用 Xcode 的 Memory Graph Debugger(内存图调试器)——运行 App,点击内存图按钮,泄漏的对象会用紫色标记。也可以用 Instruments 的 Leaks 模板。
Q 所有属性都声明为 weak 可以吗?
A 不可以。weak 意味着"不拥有",如果所有属性都 weak,对象会立即释放。必须有强引用链从根对象(AppDelegate、Window)连接到所有活跃对象。
📖 小节
- ARC 自动管理类实例的内存,引用计数归零时释放
- 循环引用指两个对象互相强持有,导致谁都无法释放
- 弱引用(weak)不增加引用计数,对象释放后自动置 nil
- 无主引用(unowned)不增加引用计数,不置 nil,访问已释放对象会崩溃
- 闭包捕获列表
[weak self]破除闭包造成的循环引用 - 用 Xcode Memory Graph Debugger 可以可视化检测循环引用
📝 作业
- 基础题: 创建两个类
Apartment和Tenant,Tenant 持有所租公寓,Apartment 持有租户列表。使用 weak 避免循环引用,验证 deinit 正常触发。 - 进阶题: 写一个
TimerManager类,内部持有Timer并使用闭包作为回调。在闭包中访问 self 的属性和方法,用捕获列表避免循环引用。 - 挑战题: 模拟一个常见泄漏场景——在视图控制器中发起网络请求,在闭包回调中更新 UI。构造泄漏版本然后修复。用 deinit 打印验证修复是否有效。