这条在讲 Swift 类的 deinit 和 ARC 怎么自动清理内存——出处是 dev.to 上 Gamya 发的,7 月 22 日,标签 swift、programming、ios。
我一开始以为又是那种把官方文档中译一遍的 ARC 科普。这类东西 Swift 社区里十个作者九个在写,点开前基本能猜到内容。但读到 TrainingCamp 示例那一段,我改主意了。
作者搞了三个变量:camp1 和 camp3 都指向同一个 Konoha 训练场实例,camp2 指向另一个沙村营地实例。然后依次置 nil,看 deinit 什么时候被调用。先把 camp2 置 nil,沙村营地的 deinit 立即触发,因为 camp2 是唯一引用。但把 camp1 置 nil 时,Konoha 训练场的 deinit 没动静,因为 camp3 还攥着一个引用。等 camp3 也置 nil 了,Konoha 的 deinit 才执行。
这个双实例对比把「引用计数归零时 deinit 才被调用」这条机制讲得比官方文档那句「当引用计数达到零时」清楚多了。原文是这样写的——两个实例并排走,一个先释放一个后释放,你看着 deinit 的触发时机,引用计数那个抽象概念就落下来了。
还有一段讲 let 的,用 Ninja 例子,也值得读。let naruto 是 Ninja 实例,powerLevel 9000,然后 naruto.powerLevel = 9001 没问题,但给 naruto 重新赋一个新的 Ninja 实例不行。五行代码把「let 锁定的是引用而不是实例数据」说清了。我顺手跑了一下:
final class Ninja {
var powerLevel: Int
init(powerLevel: Int) { self.powerLevel = powerLevel }
}
let naruto = Ninja(powerLevel: 9000)
naruto.powerLevel = 9001 // 允许
// naruto = Ninja(powerLevel: 8000) // 编译错误
跟原文一致,跑通了。let 锁的是引用本身,不是实例的 mutable 属性。
然后评论区给了我一点意外。
Vinicius Pereira 把 Swift ARC 和 CPython 的引用计数做了对照。CPython 在引用计数之外有 tracing cycle collector 处理循环引用,PEP 442 在 Python 3.4 补上了 finalizer safety;Swift 这边没这层,断循环引用只能靠 weak 和 unowned。另一个 Mustafa ERBAY 说得更直接:deinit 并不保证会被调用,因为强引用环会让引用计数永远到不了零。作者 Gamya 在回复里承认,强引用环是这篇文章的明显缺口。这段对照我读下来,比正文本身更短、更硬。如果你已经知道 ARC 是怎么回事,正文的增量其实不大——写得清楚,但那些官方文档里都有。评论区这个 CPython 对照,是我读过的关于 Swift ARC 最短的对标解释。
文章里有个地方确实没讲透。作者列了需要 deinit 的场景——网络连接、文件句柄、观察者、定时器——但一个都没展开。作者自己也说,平时只写 SwiftUI 的话很少需要主动写 deinit,但理解内存模型有助于搞清 runtime 在做什么。话是对的,可场景列表一笔带过,就有点隔靴搔痒。
这篇我放进了夹子的 Swift 机制分类。下次再看到讲 ARC 的文章,我先看它有没有覆盖循环引用——没覆盖的,等于把 deinit 讲了一半。