Swift: Swift 内存管理与 ARC 教程:自动引用计数与循环引用修复

ARC 是 Swift 的内存管家——自动跟踪和管理对象的引用计数。理解它,你的 App 才不会悄悄吃掉所有内存。

1. 你将学到


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)自动跟踪每个类实例被多少个变量引用。引用计数归零时,实例自动释放。

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) 引用计数规则

操作 引用计数变化 说明
创建实例 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。

100%
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)连接到所有活跃对象。

📖 小节


📝 作业

  1. 基础题: 创建两个类 ApartmentTenant,Tenant 持有所租公寓,Apartment 持有租户列表。使用 weak 避免循环引用,验证 deinit 正常触发。
  2. 进阶题: 写一个 TimerManager 类,内部持有 Timer 并使用闭包作为回调。在闭包中访问 self 的属性和方法,用捕获列表避免循环引用。
  3. 挑战题: 模拟一个常见泄漏场景——在视图控制器中发起网络请求,在闭包回调中更新 UI。构造泄漏版本然后修复。用 deinit 打印验证修复是否有效。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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