Swift并没有传统意义上的垃圾回收器,而是采用编译器层面的自动引用计数,也就是ARC,来接管堆上对象的内存生命周期。每一个类实例在创建时都会带有一个内部引用计数器,当有新的强引用指向它,计数加一;当某个强引用消失,计数减一。一旦计数变为零,系统立刻回收这块内存,同时调用析构器。这套机制对开发者基本透明,不需要手动调用retain或者release,但理解它的计数变化规律,是写出稳定程序的前提。

ARC底层工作原理与引用计数规则
在编译阶段,Swift编译器会在每个可能引起引用变化的时机自动插入维护计数的逻辑。比如把一个对象赋值给变量、当作参数传入函数、或者存进数组,都会让强引用计数增加;而当变量离开作用域、被重新赋值、或者容器移除元素,计数相应减少。值得注意的是,只有类类型受ARC管理,结构体、枚举等值类型由于默认在栈上拷贝,不涉及引用计数。这也是为什么Swift推荐优先使用值类型来减少隐式共享带来的复杂度。
我们可以借助一个最简例子观察引用关系。下面代码中,person变量第一次指向实例时计数为一,第二个变量指向同一实例后计数变为二,函数结束后其中一个局部变量销毁,计数回落,但只有两个引用都断开,实例才会真正释放并打印析构信息。
class Person {
let name: String
init(name: String) {
self.name = name
print("(name) 被初始化")
}
deinit {
print("(name) 被释放")
}
}
func demo() {
let p1 = Person(name: "张三")
let p2 = p1
// 此时 Person 实例强引用计数为 2
}
demo()
// 函数退出后 p1、p2 均销毁,计数归零,触发 deinit
这种基于计数的方式性能很高,没有全局扫描开销,但也带来一个本质缺陷:只要引用关系形成闭环,计数就永远无法归零。因此开发者必须主动识别闭环并在合适位置插入非强引用,这正是后续weak与unowned存在的意义。
循环引用的常见场景与问题代码
最常见的循环引用发生在两个类实例互相持有对方强引用的时候。比如一个客户对象保存了对应的银行卡对象,而银行卡又反过来强引用客户,离开作用域后两者计数都停留在一,内存泄漏。另一种更隐蔽的场景是闭包捕获:闭包默认对其内部使用的外部变量进行强捕获,如果闭包又被该变量自己所持有的属性引用,就形成了自我闭环。很多网络回调、动画 completion 里的泄漏都源于此。
下面展示一个典型的双向强引用问题。Customer持有Card,Card的owner又指回Customer,demo函数结束后两个对象都没有释放,控制台不会打印任何析构消息,说明泄漏已经发生。
class Customer {
let id: Int
var card: Card?
init(id: Int) {
self.id = id
}
deinit { print("Customer (id) 释放") }
}
class Card {
let number: String
var owner: Customer?
init(number: String) {
self.number = number
}
deinit { print("Card (number) 释放") }
}
func demo() {
let c = Customer(id: 1)
let card = Card(number: "6222")
c.card = card
card.owner = c
}
demo()
// 此处无释放日志,发生循环引用
闭包造成的循环引用也极其普遍。例如某个控制器有一个完成处理的闭包属性,而闭包内部又使用了self,同时闭包被控制器自身存储,就会互相牵制。解决思路同样是打破强闭环,只不过语法上要在闭包参数列表前使用捕获列表来声明弱或无主捕获。
weak与unowned的正确选择和使用方式
weak声明的引用不会增加实例的引用计数,而且因为被指向的对象可能随时释放,所以weak变量必须是可选类型。当目标对象销毁后,Swift运行时会自动把weak引用置为nil,因此访问它是安全的,只需做解包判断。它适合两个对象生命周期相对独立、可能一方先消失的场景,比如视图对控制器的弱引用、委托模式中的delegate。
unowned同样不增加计数,但它假设引用对象在自己访问时一定还活着,因此类型是非可选的。如果对象已经释放而你又访问了unowned属性,程序会直接崩溃。它适用于两个实例生命周期严格绑定、同时生灭的情况,例如信用卡必定有持卡人且持卡人未销户前卡不离场。下面用weak修复前面的相互引用问题,仅把Card的owner改为弱引用即可打破闭环。
class Customer {
let id: Int
var card: Card?
init(id: Int) { self.id = id }
deinit { print("Customer (id) 释放") }
}
class Card {
let number: String
weak var owner: Customer?
init(number: String) { self.number = number }
deinit { print("Card (number) 释放") }
}
func demo() {
let c = Customer(id: 1)
let card = Card(number: "6222")
c.card = card
card.owner = c
}
demo()
// 现在会正常打印两个释放日志
在闭包中则要通过捕获列表区分使用。若闭包可能在self销毁后才执行,用[weak self]并在内部解包;若闭包和self同生共死、必定在self有效期内跑完,可用[unowned self]省去解包。错误地把本应weak的地方写成unowned,是线上崩溃的高发原因,因此不确定生命周期时一律优先weak更稳妥。
实际工程中的排查与编码规范
在真实项目里,可以借助Xcode的Memory Graph Debugger抓取对象引用图,直观看到哪两个节点形成了环。配合Instrument的Leaks模板,能定位到未释放实例的创建堆栈。日常编码应当建立约定:Delegate、DataSource一律weak;闭包捕获self先思考是否需弱引用;在模型层双向关联至少有一侧是非强引用。
此外,还需注意闭包内嵌套闭包时捕获列表只作用于当前层,内层若也要用self必须再次声明。结构体嵌套类、或值类型持有引用类型时虽不形成闭环,但也可能意外延长生命周期,应审视是否必要。把这些规则沉淀为代码评审检查项,能大幅降低Swift项目中的内存隐患。
class ViewController {
var completion: (() -> Void)?
func load() {
completion = { [weak self] in
guard let self = self else { return }
self.render()
}
}
func render() { print("渲染界面") }
}
综上,Swift的ARC以编译期计数实现高效内存回收,而weak与unowned是开发者手中最直接的破环工具。理清对象从属关系、在捕获列表中谨慎声明引用方式,才能既享受自动管理便利,又避开隐蔽的泄漏与崩溃。
SwiftARCweak_unowned修改时间:2026-08-18 07:38:31