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: "张三") // 引用计数=1person = nil // 引用计数归零,触发析构器
此时输出:张三被释放
。若多个变量引用同一实例,需所有引用断开后才会释放:
var ref1: Person? = personvar ref2: personref1 = nil // 引用计数仍为1ref2 = 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 = unit4Aunit4A!.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检测循环引用。