Swift ARC:一个对象到底什么时候才会被释放?

在 iOS 开发里,我们经常会写这样的代码:

swift 复制代码
var person: Person? = Person()
person = nil

然后这个对象就"消失"了。

我们没有手动调用 free(),也没有告诉系统"现在请释放这块内存",但 Swift 却知道什么时候应该销毁对象。

再比如,你可能还见过这样的代码:

swift 复制代码
completion = { [weak self] in
    self?.loadData()
}

为什么这里要写 [weak self]

为什么不用 strong self

为什么 weak self 后面又总是跟着一个 ?

这些看起来是几个零散的语法点,其实背后都指向同一个机制:

ARC,Automatic Reference Counting,自动引用计数。

上一篇文章我们聊了 Optional:一个值"可能不存在"时,Swift 如何强迫我们显式处理。

这一篇继续往下走,看看另一个非常核心的问题:

一个对象,到底什么时候应该存在,又什么时候应该被释放?


一、先看一个最简单的问题:person = nil 后发生了什么?

先定义一个类:

swift 复制代码
final class Person {

    let name: String

    init(name: String) {
        self.name = name
        print("(name) init")
    }

    deinit {
        print("(name) deinit")
    }
}

然后:

swift 复制代码
var person: Person? = Person(name: "Jack")

person = nil

输出:

swift 复制代码
Jack init
Jack deinit

init 很容易理解,对象被创建时调用。

但比较有意思的是:

swift 复制代码
person = nil

之后,Swift 自动调用了:

swift 复制代码
deinit

也就是说,这个对象被释放了。

问题来了:

Swift 怎么知道现在可以把它释放?

答案就是 ARC。


二、ARC 到底在"数"什么?

ARC 的全称是: Automatic Reference Counting

即:

自动引用计数。

它的核心思想其实并不复杂。

对于一个类实例,Swift 会维护它被多少个强引用持有。

只要还有强引用存在,这个对象就需要继续活着。

当最后一个强引用消失以后,对象就可以被释放。

例如:

swift 复制代码
var p1: Person? = Person(name: "Jack")

可以粗略理解成:

swift 复制代码
p1
 │
 │ strong
 ▼
Person("Jack")

strong reference count = 1

这时 p1 强引用着 Person 对象。

接着:

swift 复制代码
var p2 = p1

现在又多了一个引用:

swift 复制代码
p1 ─────┐
        │
        ▼
      Person
        ▲
        │
p2 ─────┘

可以把它理解成:

swift 复制代码
strong reference count = 2

如果:

swift 复制代码
p1 = nil

这里只断掉了一条引用:

swift 复制代码
strong reference count

2 → 1

对象不会被释放,因为:p2 还在引用它。

只有继续:

swift 复制代码
p2 = nil

最后一个强引用也没了:

swift 复制代码
1 → 0

这时对象才会执行: deinit 然后释放。

所以理解 ARC 有一句非常重要的话:

ARC 关心的不是"变量还在不在",而是对象是否仍然被强引用持有。


三、为什么我们平时几乎看不到 strong

在 Objective-C 里,我们经常能看到:

objectivec 复制代码
@property (nonatomic, strong) Person *person;

但 Swift 里通常只是:

swift 复制代码
var person: Person?

没有:

swift 复制代码
strong var person: Person?

原因是:

Swift 对普通引用默认就是强引用。

例如:

swift 复制代码
final class ViewController {

    var person: Person?

}

只要: person 指向一个 Person 对象,这个属性就会强引用该对象。

为什么默认要这么做?

因为最正常的逻辑就是:

"只要我还持有这个对象,就说明我还需要它。"

如果普通引用默认不延长对象生命周期,那么就很容易出现"代码还准备使用对象,但对象已经被释放"的问题。

所以: strong 本身不是坏事。

恰恰相反,绝大多数引用都应该是 strong。

真正的问题发生在:

两个对象互相 strong。


四、两个对象为什么会"谁也不肯死"?

来看一个经典例子。

我们有一个人:

swift 复制代码
final class Person {

    let name: String

    var apartment: Apartment?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("Person (name) deinit")
    }
}

还有一个公寓:

swift 复制代码
final class Apartment {

    let number: String

    var tenant: Person?

    init(number: String) {
        self.number = number
    }

    deinit {
        print("Apartment (number) deinit")
    }
}

创建:

swift 复制代码
var person: Person? = Person(name: "Jack")
var apartment: Apartment? = Apartment(number: "101")

这时候:

swift 复制代码
person ─────► Person

apartment ──► Apartment

接着建立关系:

swift 复制代码
person?.apartment = apartment
apartment?.tenant = person

现在就变成:

swift 复制代码
Person ───strong───► Apartment
  ▲                    │
  │                    │
  └─────strong─────────┘

Person 强引用 Apartment

Apartment 又强引用 Person

这时,我们把外部变量全部置空:

swift 复制代码
person = nil
apartment = nil

按直觉来说:

我都已经不用这两个对象了,它们应该被释放吧?

但运行之后你会发现:什么都没有输出。

Persondeinit 没执行。

Apartmentdeinit 也没执行。

为什么?因为虽然:

swift 复制代码
外部 → Person

这条引用没有了,但是:

swift 复制代码
Apartment → Person

仍然存在。

同样:

swift 复制代码
外部 → Apartment

虽然没有了,但:

swift 复制代码
Person → Apartment

仍然存在。

最终两个对象变成:

swift 复制代码
┌─────────────────────────┐
│                         ▼
Person ───────────────► Apartment
  ▲                         │
  └─────────────────────────┘

谁都不肯放开谁。于是两边的强引用数量都无法降到 0。这就是:

强引用循环

也就是我们常说的: retain cycle


五、weak 到底解决了什么?

现在思考一个现实问题。

一个 Person 可以拥有一个 Apartment

但从 Apartment 的角度来说:

tenant 只是当前住户,真的有必要强行保证 Person 永远活着吗?

很多时候答案是否定的。

所以可以把:

swift 复制代码
var tenant: Person?

改成:

swift 复制代码
weak var tenant: Person?

完整代码:

swift 复制代码
final class Apartment {

    let number: String

    weak var tenant: Person?

    init(number: String) {
        self.number = number
    }

    deinit {
        print("Apartment (number) deinit")
    }
}

这时候引用关系变成:

swift 复制代码
Person ───strong───► Apartment
  ▲                    │
  │                    │
  └──────weak──────────┘

最大的区别是:

swift 复制代码
weak var tenant

不会增加 Person 的强引用计数。

所以当:

swift 复制代码
person = nil

外部的最后一个强引用断掉以后:

swift 复制代码
Person 的强引用数 → 0

于是:

swift 复制代码
deinit

正常执行。

Person 被释放以后,又会失去对 Apartment 的强引用。

最后:

swift 复制代码
Apartment 的强引用数 → 0

Apartment 也被释放。

循环被打破了。

所以 weak 的本质不是:

"可以防止内存泄漏的神奇关键字。"

它真正做的事情只是:

我可以指向这个对象,但我不负责延长它的生命周期。


六、为什么 weak 总是和 Optional 搞在一起?

这里正好可以和上一篇 Optional 联系起来。

我们定义:

swift 复制代码
weak var tenant: Person?

为什么必须是:

swift 复制代码
Person?

而不是:

swift 复制代码
Person

原因就在于:

被 weak 引用的对象可能随时释放。

例如:

swift 复制代码
var person: Person? = Person(name: "Jack")

apartment.tenant = person

此时:

swift 复制代码
apartment.tenant

有值。

然后:

swift 复制代码
person = nil

如果这已经是最后一个强引用,那么 Person 对象会被释放。

随后 Swift 会自动把:

swift 复制代码
apartment.tenant

设置成:

swift 复制代码
nil

这叫:

zeroing weak reference(弱引用自动置空)

所以:

swift 复制代码
weak var tenant: Person?

本质上是在告诉 Swift:

"这里现在可能有一个 Person,但以后这个 Person 被释放以后,这里就变成 nil。"

是不是和 Optional 的语义一下连起来了?

上一篇我们说:

swift 复制代码
String?

表示:

swift 复制代码
String 或 nil

而这里:

swift 复制代码
weak var tenant: Person?

表示:

swift 复制代码
Person 或 nil

区别只是:

这个 nil 不一定是你手动赋值的,也可能是 ARC 在对象销毁时自动帮你设置的。

所以 Optional 不只是 API 返回值可能为空时才有用。

它也深深参与了 Swift 的内存管理。


七、那 unowned 又是什么?

既然 weak 不增加强引用计数,还有一个非常像它的东西:

swift 复制代码
unowned

例如:

swift 复制代码
unowned var owner: Person

unowned 同样:

不增加对象的强引用计数。

但它和 weak 有一个非常大的区别:

unowned 默认认为它引用的对象一定还活着。

简单比较一下:

特性 strong weak unowned
增加强引用计数
延长对象生命周期
对象释放后自动变 nil ---
一般是否 Optional 可以 通常不是
生命周期要求 普通持有 对象可能先释放 必须保证引用有效

假设:

swift 复制代码
unowned var owner: Person

如果 Person 已经被释放,你之后仍然访问:

swift 复制代码
owner

程序就可能直接崩溃。

所以使用 unowned 的前提是:

你非常明确地知道,被引用对象一定比当前引用活得更久。

如果你不能百分之百确认这一点,

那么:

swift 复制代码
weak

通常更安全。


八、为什么闭包特别容易造成循环引用?

到了真实 iOS 开发里,比 PersonApartment 更常见的,其实是:

对象 + Closure。

来看一个例子:

swift 复制代码
final class ViewModel {

    var completion: (() -> Void)?

    func start() {
        completion = {
            self.loadData()
        }
    }

    func loadData() {
        print("load data")
    }

    deinit {
        print("ViewModel deinit")
    }
}

然后:

swift 复制代码
var viewModel: ViewModel? = ViewModel()

viewModel?.start()

viewModel = nil

你可能会发现:

swift 复制代码
ViewModel deinit

没有输出。

为什么?

先看:

swift 复制代码
var completion: (() -> Void)?

ViewModel 持有:

swift 复制代码
completion

也就是 Closure。

可以理解成:

swift 复制代码
ViewModel ───strong───► Closure

而 Closure 中:

swift 复制代码
self.loadData()

又捕获了 self

默认情况下,Closure 会强引用捕获的对象。

于是又出现:

swift 复制代码
ViewModel ───strong───► Closure
   ▲                      │
   │                      │
   └──────strong──────────┘

熟悉的结构又出现了。

这和刚才:

swift 复制代码
Person → Apartment → Person

没有本质区别。

只不过现在变成:

swift 复制代码
self → Closure → self

于是,即使:

swift 复制代码
viewModel = nil

外部引用已经消失,内部仍然互相持有。

ViewModel 就无法释放。


九、[weak self] 到底在干什么?

这时候我们把:

swift 复制代码
completion = {
    self.loadData()
}

改成:

swift 复制代码
completion = { [weak self] in
    self?.loadData()
}

这里:

swift 复制代码
[weak self]

叫:

Capture List,捕获列表。

它是在告诉 Closure:

你可以捕获 self,但不要强引用它。

于是关系变成:

swift 复制代码
ViewModel ───strong───► Closure
   ▲
   │
  weak
   │
Closure

循环自然就断掉了。

当:

swift 复制代码
viewModel = nil

最后一个强引用消失后:

swift 复制代码
ViewModel deinit

可以正常执行。

所以:

swift 复制代码
[weak self]

其实一点都不神秘。

它只是一个非常具体的 ARC 操作:

改变 Closure 捕获 self 时使用的引用关系。


十、为什么 [weak self] 后面又出现 self?

代码通常是:

swift 复制代码
completion = { [weak self] in
    self?.loadData()
}

为什么不是:

swift 复制代码
self.loadData()

因为:

swift 复制代码
[weak self]

以后,Closure 不再负责保证 self 活着。

也就是说:当 Closure 真正执行的时候,self 有可能已经被释放。于是这里的 self 实际上是:

swift 复制代码
Self?

也就是 Optional。

所以:

swift 复制代码
self?.loadData()

其实和上一篇文章讲的 Optional Chaining 完全是同一套机制:

swift 复制代码
如果 self 还活着
→ 调用 loadData()

如果 self 已经释放
→ 什么都不做

ARC 和 Optional 到这里真正连接起来了。


十一、看到 Closure 就写 [weak self],其实也不对

很多人第一次学循环引用以后,会形成一个习惯:

swift 复制代码
{
    self.doSomething()
}

看到了 self,马上改:

swift 复制代码
{ [weak self] in
    self?.doSomething()
}

最后项目里到处都是:

swift 复制代码
[weak self]

但这其实是一种比较机械的写法。

真正应该判断的不是:

Closure 有没有用到 self?

而是:

这个 Closure 的生命周期是什么?谁持有它?它和 self 会不会构成强引用环?

比如:

swift 复制代码
UIView.animate(withDuration: 0.3) {
    self.view.alpha = 0
}

这个 Closure 会被动画 API 暂时持有。

动画结束以后,它就会被释放。

通常不存在:

swift 复制代码
self 长期持有 Closure

这样的引用链。

因此这里一般不需要因为看见 self 就机械地写:

swift 复制代码
[weak self]

但如果是:

swift 复制代码
self.completion = {
    self.doSomething()
}

就需要特别警惕。

因为:

swift 复制代码
self
 │
 ▼
completion
 │
 ▼
Closure
 │
 ▼
self

整个环非常明显。

所以判断循环引用时,真正应该画的是: 谁持有谁?

而不是简单记:

swift 复制代码
Closure + self = weak self

这两个思路差别很大。


十二、为什么 delegate 经常写成 weak

理解 ARC 以后,你会发现很多以前"背下来"的 iOS 写法突然有理由了。

比如:

swift 复制代码
weak var delegate: SomeDelegate?

为什么 delegate 经常是 weak?

假设:

swift 复制代码
final class CustomView {

    var delegate: CustomViewDelegate?

}

而:

swift 复制代码
ViewController

持有:

swift 复制代码
CustomView

与此同时:

swift 复制代码
CustomView.delegate

又指向:

swift 复制代码
ViewController

于是:

swift 复制代码
ViewController ──strong──► CustomView
      ▲                       │
      │                       │
      └──────strong───────────┘

又是一个循环。

所以通常 delegate 会设计成:

swift 复制代码
weak var delegate: CustomViewDelegate?

变成:

swift 复制代码
ViewController ──strong──► CustomView
      ▲
      │
     weak
      │
      └───────────────────────

循环被打断。

这也是为什么 Objective-C / Swift 面试里经常会问:

为什么 delegate 一般使用 weak?

因为:

delegate 往往存在反向引用关系,如果使用 strong,可能和宿主对象形成强引用循环。


十三、一个对象到底什么时候才会被释放?

到这里,我们终于可以回来回答标题里的问题。

假设:

swift 复制代码
let object = SomeClass()

对象什么时候释放,并不是由:

swift 复制代码
作用域结束

这一件事决定的。

真正核心的是:

什么时候不再存在需要维持这个对象生命周期的强引用。

只要还有强引用(strong reference),它就需要继续存在。

当最后一个强引用被解除后,ARC 就可以销毁这个实例,并调用:deinit

所以:

swift 复制代码
object = nil

本身不是某种"释放对象"的函数。

它只是:

删除了一个强引用。

如果这恰好是最后一个强引用,那么对象会释放。

如果还有:

swift 复制代码
p2
Closure
Timer
其他对象属性

在强引用它,那么对象仍然不会销毁。

另外,Swift 编译器会参与对象生命周期优化,因此不要把 ARC 简单理解成一个能随时读取的"计数器变量"。我们平时用"引用计数 +1/-1"来帮助理解是没问题的,但更准确的工程视角应该是:

ARC 根据对象的强引用生命周期自动插入并优化 retain/release 行为。


十四、怎么判断自己的对象到底有没有释放?

最简单的方法其实就是:

swift 复制代码
deinit {
    print("ViewController deinit")
}

如果一个页面退出以后,你预期它应该销毁,却始终没有看到:

swiftswiftswift 复制代码
ViewController deinit

那就应该开始怀疑:

swiftswift 复制代码
是不是还有东西持有它?

比如:

swift 复制代码
Closure
delegate
Timer
Notification
其他对象属性

在实际项目里,还可以使用 Xcode 的:

swift 复制代码
Memory Graph Debugger

观察对象之间的引用关系。

当出现循环引用时,它比盯着几十个 weak self 猜问题有效得多。

如果要进一步分析内存泄漏,也可以使用 Instruments 中的 Leaks 等工具。

理解 ARC 以后,这些工具也不会再只是:

"点一下看看有没有红色报警。"

你会知道自己真正要找的是:

swift 复制代码
谁本应该释放,却还被谁强引用着?

十五、strong、weak、unowned 最终不是三个需要死记硬背的关键字

如果只是背定义:

swift 复制代码
strong:强引用

weak:弱引用

unowned:无主引用

其实很容易忘。

更容易理解的方式是:

swift 复制代码
strong
↓
我需要这个对象继续活着
swift 复制代码
weak
↓
我想访问这个对象
但我不负责让它继续活着
而且它以后可能不存在
swiftswift 复制代码
unowned
↓
我不负责让它继续活着
但我确信访问它的时候它一定存在

一旦从"对象生命周期责任"的角度去理解,这三个词就不再是孤立的 Swift 语法。


写在最后

ARC 其实并没有想象中那么神秘。

它解决的核心问题只有一个:

如何判断一个对象什么时候已经不再需要,可以安全地释放。

Swift 默认通过 strong 保证对象在需要的时候继续存活。

当两个对象互相 strong 时,就可能产生循环引用。

weakunowned 的作用,本质上都是在某些引用关系中告诉 ARC:

这个引用不应该决定对象的生命周期。而 [weak self]weak delegate 这些我们在 iOS 项目里随处可见的写法,本质上也都只是这一规则在不同场景下的应用。

理解了这一点以后,再看到:

swift 复制代码
weak var delegate: SomeDelegate?

或者:

swift 复制代码
{ [weak self] in
    self?.doSomething()
}

就不需要再靠背了。

真正应该问的是:

谁持有谁? 谁应该负责保证谁活着? 这里会不会形成一个强引用闭环?

把这三个问题想清楚,ARC 基本也就想清楚了。

相关推荐
ClouGence2 小时前
GPT-6 做 UI 自动化测试:Demo 惊艳,但真的适合长期回归吗?
前端·chatgpt·测试
阿酷tony2 小时前
视频专栏列表的防录屏水印和跑马灯效果(也可网站调用)
java·前端·音视频
凌风的跨境分享3 小时前
Temu店群运维提效:定时策略自动化任务全场景实操指南
大数据·运维·前端·人工智能·架构·自动化
linux_cfan3 小时前
videojs v10 源代码系列解读:06 · DOM 工具箱:事件、聚焦、Shadow DOM、定位
前端
尾善爱看海3 小时前
Vue 面试进阶篇:Composition API、插槽、自定义指令……8 个章节 + 高频面试题全解析
前端·javascript·vue.js·面试·vue
SEO_juper3 小时前
外贸多语言站最隐蔽的流量杀手:hreflang 错了,谷歌把德语页推给美国人(附审计脚本)
开发语言·前端·python·seo·独立站·谷歌优化
亿元程序员4 小时前
为什么现在 AI 这么发达了,还要坚持手搓教程?
前端
三小河4 小时前
在 Codex 桌面端接入 DeepSeek 模型(CC Switch 代理中转)
前端·javascript·人工智能
CoderYanger4 小时前
前端基础——JavaScript(WebAPI)代码案例
java·开发语言·前端·javascript·css·前端框架·html5