Swift 中,自动引用计数(Automatic Reference Counting, ARC) 通过跟踪每个实例的引用数量,自动释放不再需要的对象内存,从而避免手动管理内存的复杂性。然而,理解 ARC 的工作原理及如何处理对象间的关系,是防止内存泄漏和构建高效应用的关键。
原理
核心机制是:每当创建一个类实例时,系统会分配内存存储其属性及类型信息,并维护一个引用计数器。当实例被赋值给变量、常量或属性时,会建立强引用(Strong Reference),引用计数加1;当引用断开时,计数减1。当计数归零时,实例被释放。
示例:
class Person {
let name: String
init(name: String) { self.name = name }
deinit { print("\(name)被释放") }
}
var person: Person? = Person(name: "张三") // 引用计数=1
person = nil // 引用计数归零,触发析构器
此时输出:张三被释放
。若多个变量引用同一实例,需所有引用断开后才会释放:
var ref1: Person? = person
var ref2: person
ref1 = nil // 引用计数仍为1
ref2 = nil // 计数归零,释放实例
循环强引用
当两个类实例互相持有对方的强引用时,会导致循环强引用(Retain Cycle),使ARC无法正确释放内存。
场景示例:
class Person {
var apartment: Apartment?
}
class Apartment {
var tenant: Person?
}
var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john // 互相强引用
john = nil // 未触发析构
unit4A = nil // 仍未被释放
此时john
和unit4A
的引用计数始终为1,导致内存泄漏。
弱引用与无主引用
Swift提供了两种方案打破循环引用:
弱引用(Weak Reference)
适用于生命周期较短的实例。弱引用不会增加引用计数,且当目标实例释放时自动置为nil
。class Apartment { weak var tenant: Person? // 弱引用 }
当
john = nil
时,tenant
自动置为nil
,Apartment
实例随后被释放。无主引用(Unowned Reference)
适用于生命周期相同或更长的实例。与弱引用不同,无主引用始终假定有值,不会自动置为nil
。class CreditCard { unowned let customer: Customer // 无主引用 } class Customer { var card: CreditCard? }
CreditCard
实例的生命周期不会超过关联的Customer
,因此可安全使用无主引用。
闭包中的循环引用与捕获列表
闭包作为引用类型,若在内部捕获self
,也可能导致循环引用。例如:
class HTMLElement {
lazy var asHTML: () -> String = {
return "<\(self.name)>\(self.text ?? "")</\(self.name)>"
}
// ...
}
此时闭包持有self
的强引用,而self
也通过属性持有闭包,形成循环。
解决方案:使用捕获列表声明unowned
或weak
引用:
lazy var asHTML: () -> String = { [unowned self] in
return "<\(self.name)>\(self.text ?? "")</\(self.name)>"
}
通过[unowned self]
,闭包不再强持有self
,打破循环。
特殊场景:隐式解包可选与无主引用
在需要确保初始化顺序的场景中,可结合隐式解包可选类型和无主引用:
class Country {
var capitalCity: City! // 隐式解包可选
init(name: String, capitalName: String) {
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
unowned let country: Country // 无主引用
}
capitalCity
在Country
初始化完成后才被赋值,但通过隐式解包确保使用时非空,避免循环。
局限与最佳实践
- 仅适用于类:结构体和枚举是值类型,不参与引用计数。
- 避免过度使用
unowned
:错误使用可能导致运行时崩溃。 - 调试工具:利用Xcode的Memory Graph Debugger检测循环引用。