跳到主要内容

Swift ARC 基础

介绍

在 Swift 中,内存管理是一个非常重要的概念,尤其是在处理对象和类实例时。Swift 使用 自动引用计数(ARC,Automatic Reference Counting) 来管理应用程序的内存。ARC 会自动跟踪和清理不再需要的对象,从而避免内存泄漏。

ARC 的核心思想是:每当创建一个类的实例时,ARC 会分配一块内存来存储该实例的信息。当这个实例不再被任何变量或常量引用时,ARC 会自动释放这块内存。

备注

ARC 仅适用于类的实例。结构体(struct)和枚举(enum)是值类型,它们的实例不会被 ARC 管理。

ARC 的工作原理

ARC 通过跟踪每个类实例的引用计数来工作。每当一个实例被赋值给一个变量、常量或属性时,引用计数会增加 1。当引用该实例的变量、常量或属性被设置为 nil 或超出作用域时,引用计数会减少 1。当引用计数为 0 时,实例会被销毁,内存会被释放。

示例:引用计数

swift
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) 被初始化")
}
deinit {
print("\(name) 被销毁")
}
}

var person1: Person?
var person2: Person?
var person3: Person?

person1 = Person(name: "Alice") // 引用计数为 1
person2 = person1 // 引用计数为 2
person3 = person1 // 引用计数为 3

person1 = nil // 引用计数为 2
person2 = nil // 引用计数为 1
person3 = nil // 引用计数为 0,实例被销毁

输出:

Alice 被初始化
Alice 被销毁

在这个例子中,Person 类的实例 Alice 被三个变量引用。当所有变量都被设置为 nil 时,引用计数降为 0,实例被销毁。

强引用循环

虽然 ARC 可以自动管理内存,但在某些情况下,两个或多个对象之间可能会形成 强引用循环,导致它们无法被释放,从而引发内存泄漏。

示例:强引用循环

swift
class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
print("\(name) 被初始化")
}
deinit {
print("\(name) 被销毁")
}
}

class Apartment {
let unit: String
var tenant: Person?
init(unit: String) {
self.unit = unit
print("公寓 \(unit) 被初始化")
}
deinit {
print("公寓 \(unit) 被销毁")
}
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John") // John 被初始化
unit4A = Apartment(unit: "4A") // 公寓 4A 被初始化

john!.apartment = unit4A // John 引用公寓 4A
unit4A!.tenant = john // 公寓 4A 引用 John

john = nil // John 的引用计数为 1(因为公寓 4A 还在引用他)
unit4A = nil // 公寓 4A 的引用计数为 1(因为 John 还在引用它)

在这个例子中,Person 实例 JohnApartment 实例 unit4A 相互引用,导致它们的引用计数永远不会降为 0,从而无法被销毁。

警告

强引用循环会导致内存泄漏,因此需要特别注意。

解决强引用循环

Swift 提供了两种方式来解决强引用循环:弱引用(weak reference)无主引用(unowned reference)

弱引用

弱引用不会增加引用计数,因此不会阻止实例被销毁。弱引用通常用于可能为 nil 的情况。

swift
class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
print("\(name) 被初始化")
}
deinit {
print("\(name) 被销毁")
}
}

class Apartment {
let unit: String
weak var tenant: Person? // 使用 weak 关键字
init(unit: String) {
self.unit = unit
print("公寓 \(unit) 被初始化")
}
deinit {
print("公寓 \(unit) 被销毁")
}
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John") // John 被初始化
unit4A = Apartment(unit: "4A") // 公寓 4A 被初始化

john!.apartment = unit4A // John 引用公寓 4A
unit4A!.tenant = john // 公寓 4A 弱引用 John

john = nil // John 的引用计数为 0,实例被销毁
unit4A = nil // 公寓 4A 的引用计数为 0,实例被销毁

输出:

John 被初始化
公寓 4A 被初始化
John 被销毁
公寓 4A 被销毁

无主引用

无主引用也不会增加引用计数,但它假定引用的对象永远不会为 nil。如果引用的对象被销毁,访问无主引用会导致运行时错误。

swift
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
print("\(name) 被初始化")
}
deinit {
print("\(name) 被销毁")
}
}

class CreditCard {
let number: String
unowned let customer: Customer // 使用 unowned 关键字
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
print("信用卡 \(number) 被初始化")
}
deinit {
print("信用卡 \(number) 被销毁")
}
}

var john: Customer?
john = Customer(name: "John") // John 被初始化
john!.card = CreditCard(number: "1234-5678", customer: john!) // 信用卡 1234-5678 被初始化

john = nil // John 和信用卡都被销毁

输出:

John 被初始化
信用卡 1234-5678 被初始化
John 被销毁
信用卡 1234-5678 被销毁
提示

使用 weakunowned 时,需要根据实际情况选择合适的引用类型。

实际应用场景

在实际开发中,ARC 和引用循环的处理非常重要。例如,在 iOS 开发中,视图控制器(UIViewController)和视图(UIView)之间可能会形成强引用循环。通过使用 weakunowned,可以避免内存泄漏。

总结

  • ARC 是 Swift 中用于管理类实例内存的机制。
  • 引用计数 决定了实例何时被销毁。
  • 强引用循环 会导致内存泄漏,可以通过 weakunowned 来解决。
  • 在实际开发中,合理使用 weakunowned 是避免内存泄漏的关键。

附加资源

  • Swift 官方文档 - ARC
  • 练习:尝试创建一个包含强引用循环的示例,并使用 weakunowned 解决它。