引用计数不是垃圾回收,理解内存生命周期是 iOS 开发的分水岭。掌握 ARC 是写出高质量代码的基础。
1. ARC 工作原理
ARC (Automatic Reference Counting) 是 Swift 和 Objective-C 的内存管理机制。
引用计数基础
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) 被初始化")
}
deinit {
print("\(name) 被释放")
}
}
var person1: Person? = Person(name: "张三") // 引用计数: 1
var person2 = person1 // 引用计数: 2
var person3 = person1 // 引用计数: 3
person1 = nil // 引用计数: 2
person2 = nil // 引用计数: 1
person3 = nil // 引用计数: 0 → 调用 deinit,对象被释放
引用计数存储
Swift 对象的引用计数存储在对象头部:
┌─────────────────────────────────────┐
│ Object Header │
├─────────────────────────────────────┤
│ Strong RC │ Unowned RC │ Flags │
├─────────────────────────────────────┤
│ Object Data │
└─────────────────────────────────────┘
2. 强引用循环 (Retain Cycles)
当两个对象互相持有对方的强引用时,引用计数永远不会归零,导致内存泄漏。
典型场景
class Person {
let name: String
var apartment: Apartment?
init(name: String) { self.name = name }
deinit { print("\(name) 被释放") }
}
class Apartment {
let unit: String
var tenant: Person? // 强引用 → 循环引用!
init(unit: String) { self.unit = unit }
deinit { print("公寓 \(unit) 被释放") }
}
var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")
john?.apartment = unit4A
unit4A?.tenant = john // 形成循环引用
john = nil // Person 不会释放
unit4A = nil // Apartment 不会释放
// 内存泄漏!
解决方案:weak 引用
class Apartment {
let unit: String
weak var tenant: Person? // 弱引用,打破循环
init(unit: String) { self.unit = unit }
deinit { print("公寓 \(unit) 被释放") }
}
// 现在设置为 nil 时,两个对象都能正确释放
3. weak vs unowned
| 特性 | weak | unowned |
|---|---|---|
| 类型 | Optional | Non-optional |
| 释放后的值 | 自动设为 nil | 保持原地址(悬垂指针) |
| 访问已释放对象 | 返回 nil | 崩溃 |
| 使用场景 | 可能比自己先释放 | 确定不会比自己先释放 |
| 性能 | 略慢(需要维护弱引用表) | 略快 |
weak 使用示例
class Customer {
let name: String
var card: CreditCard?
init(name: String) { self.name = name }
deinit { print("\(name) 被释放") }
}
class CreditCard {
let number: UInt64
weak var customer: Customer? // 客户可能先被释放
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("卡号 \(number) 被释放") }
}
unowned 使用示例
class Country {
let name: String
var capitalCity: City! // 隐式解包可选
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country // 国家一定比城市存在更久
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
4. 闭包中的循环引用
闭包会捕获其引用的对象,这是循环引用的常见来源。
问题场景
class ViewController: UIViewController {
var handler: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
handler = {
// self 被强引用捕获
self.doSomething() // 循环引用!
}
}
func doSomething() { }
deinit {
print("ViewController 被释放") // 永远不会调用
}
}
解决方案:捕获列表
class ViewController: UIViewController {
var handler: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
// 使用 [weak self] 打破循环
handler = { [weak self] in
guard let self = self else { return }
self.doSomething()
}
// 或者使用 [unowned self](确保 self 一定存在)
handler = { [unowned self] in
self.doSomething()
}
}
}
捕获多个值
class DataManager {
var data: [String] = []
var processor: DataProcessor?
func setup() {
processor?.onComplete = { [weak self, weak processor] result in
self?.data = result
processor?.cleanup()
}
}
}
5. 常见内存泄漏场景
1. Timer 强引用
class TimerViewController: UIViewController {
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
// ❌ 错误:Timer 强引用 self
timer = Timer.scheduledTimer(
timeInterval: 1.0,
target: self,
selector: #selector(tick),
userInfo: nil,
repeats: true
)
}
// ✅ 正确:使用闭包版本
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.tick()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
timer?.invalidate()
}
@objc func tick() { }
}
2. NotificationCenter
class NotificationViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// iOS 9+ 自动移除观察者,但使用闭包时要注意
NotificationCenter.default.addObserver(
forName: .someNotification,
object: nil,
queue: .main
) { [weak self] notification in
self?.handleNotification(notification)
}
}
}
3. Delegate 循环
// ❌ 错误:强引用 delegate
protocol DataSourceDelegate: AnyObject { }
class DataSource {
var delegate: DataSourceDelegate? // 应该是 weak
}
// ✅ 正确
class DataSource {
weak var delegate: DataSourceDelegate?
}
6. 检测内存泄漏
Xcode Memory Graph
- 运行 App
- 点击 Debug Navigator
- 点击 “Debug Memory Graph” 按钮
- 查看对象引用关系
Instruments - Leaks
- Product → Profile (⌘I)
- 选择 “Leaks” 模板
- 运行并观察泄漏
代码检测
// 在 deinit 中打印,确认对象被释放
class MyViewController: UIViewController {
deinit {
print("✅ MyViewController 被正确释放")
}
}
7. 值类型 vs 引用类型
值类型(Struct, Enum)
struct Point {
var x: Double
var y: Double
}
var p1 = Point(x: 0, y: 0)
var p2 = p1 // 复制,不是引用
p2.x = 10
print(p1.x) // 0(不受影响)
引用类型(Class)
class PointClass {
var x: Double
var y: Double
init(x: Double, y: Double) {
self.x = x
self.y = y
}
}
var p1 = PointClass(x: 0, y: 0)
var p2 = p1 // 引用同一对象
p2.x = 10
print(p1.x) // 10(被修改)
选择建议
| 场景 | 推荐类型 |
|---|---|
| 数据模型(不需要继承) | Struct |
| 需要继承 | Class |
| 需要 identity(===) | Class |
| 需要 deinit | Class |
| 跨线程共享 | Actor |
8. 最佳实践
✅ 推荐做法
// 1. Delegate 使用 weak
protocol ViewDelegate: AnyObject { }
class View {
weak var delegate: ViewDelegate?
}
// 2. 闭包使用捕获列表
completion = { [weak self] in
self?.handleComplete()
}
// 3. 在 deinit 中清理资源
deinit {
NotificationCenter.default.removeObserver(self)
timer?.invalidate()
}
// 4. 优先使用值类型
struct User {
let id: UUID
var name: String
}
❌ 避免的做法
// 1. 不要在闭包中直接使用 self
handler = {
self.doSomething() // ❌ 可能循环引用
}
// 2. 不要忘记 invalidate Timer
// timer 一直运行,ViewController 无法释放
// 3. 不要过度使用 unowned
// 如果不确定生命周期,使用 weak 更安全