5月27日 10:40

Swift 内存管理怎么做?ARC 和循环引用详解

Swift 用 ARC(自动引用计数)管理内存。每次创建类实例,ARC 分配内存并将引用计数置为 1;每多一个强引用指向它,计数 +1;引用离开作用域或被赋新值,计数 -1;计数归零,ARC 立刻释放内存。ARC 只管引用类型(class),值类型(struct/enum)不存在引用计数。

循环引用是 ARC 最大的坑:两个实例互相强引用,计数永远不归零,内存泄漏。两种经典场景——类属性互相引用,和闭包捕获 self。

类属性互引用的解法:把一边改成 weak 或 unowned。weak 必须是可选型 var,引用的对象释放后自动变 nil,安全;unowned 不是可选型,对象释放后访问会 crash,但性能更好。选哪个看生命周期——如果被引用对象可能先死,用 weak;如果确定被引用对象活得比自己久,用 unowned。

闭包捕获 self 导致循环引用更隐蔽:闭包强引用了 self,self 又持有闭包。解法是捕获列表 [weak self][unowned self],在闭包内用 guard let self = self 解包。实际项目中 90% 用 weak self,因为闭包执行时 self 可能已释放。

追问

weak 和 unowned 有什么区别?怎么选?

weak 修饰的属性在对象释放后自动置 nil,必须是可选型 var,访问安全;unowned 不会自动置 nil,对象释放后访问触发野指针 crash。选择标准:被引用对象可能先于自己释放用 weak(比如 delegate 模式),确定对方活得比自己久用 unowned(比如 Customer 持有 CreditCard,CreditCard 无主引用 Customer)。拿不准就用 weak,安全第一。

weak 引用的底层实现是什么?

Runtime 维护一张 weak 表(哈希表),key 是对象地址,value 是指向该对象的所有 weak 指针数组。对象 dealloc 时,Runtime 遍历 weak 表找到对应的指针数组,逐个置 nil,然后从表中删除。这也是为什么 weak 访问需要加锁——多线程可能同时在读 weak 指针和修改 weak 表。

weak-strong dance 是什么?为什么需要?

闭包中 [weak self] 后,self 变成可选型,每次用都要解包。如果闭包执行期间 self 被释放,中途解包失败会导致逻辑断裂。weak-strong dance 的做法:在闭包开头 guard let self = self else { return },把 weak 引用提升为局部强引用。这样闭包执行期间 self 不会被释放,逻辑连贯,执行结束后局部强引用消失,不影响释放。

怎么检测循环引用?

Xcode 的 Memory Graph(Debug Memory Graph 按钮)可以直接看对象引用关系,找到循环引用链。Instruments 的 Leaks 工具可以自动检测泄漏,切换到 Cycles & Roots 视图能看到循环引用的图形化展示。日常开发中,在 deinit 里打个 log,如果控制器退出后没触发,大概率有循环引用。

ARC 和 MRC 有什么区别?ARC 做了什么优化?

MRC 手动写 retain/release/autorelease,ARC 由编译器自动插入这些调用。ARC 的优化:编译器会省略不必要的 retain/release——比如函数返回值直接传递给调用方,中间不需要 retain 再 release(快速路径优化)。ARC 不等于垃圾回收,它在编译期确定,没有运行时停顿。

写段代码

swift
class ViewController: UIViewController { var onComplete: (() -> Void)? func setup() { // 循环引用:闭包捕获 self,self 持有闭包 onComplete = { self.dismiss(animated: true) // ⚠️ 循环引用 } // 解法:weak self + guard onComplete = { [weak self] in guard let self = self else { return } self.dismiss(animated: true) // ✅ 安全 } } } // delegate 用 weak 防止循环引用 class ListView: UIView { weak var delegate: ListViewDelegate? }
标签:Swift