原文:ARC and Memory Management in Swift
在本教程中,你将学习 ARC 是如何工作的,以及如何在 Swift 中编程以优化内存管理。你将学习什么是引用循环,如何使用 Xcode 10 可视化调试器在引用循环发生时发现它们,以及如何打破实际示例中的引用循环。
作为一种现代化的高级编程语言,Swift 处理了你的应用程序的大部分内存管理,并代替你分配或取消分配内存。它使用 Clang 编译器的一项功能,即自动引用计数,或 ARC。在本教程中,你将学习所有关于 ARC 和 Swift 中的内存管理。
通过对这个系统的了解,你可以影响堆对象的生命何时结束。Swift 使用 ARC 在资源受限的环境中具有可预测性和效率。
ARC 自动工作,所以你不需要参与引用计数,但你确实需要考虑对象之间的关系,以避免内存泄漏。这是一项重要的要求,经常被新手开发者所忽视。
在本教程中,你将通过学习以下内容提高你的 Swift 和 ARC 技能:
- ARC 是如何工作的。
- 什么是引用循环以及如何打破引用循环。
- 实践中的引用循环的例子。
- 如何使用最新的 Xcode 可视化工具检测引用循环。
- 如何处理混合值和引用类型。
入门指南
点击本教程顶部或底部的下载材料按钮。在名为 Cycles 的文件夹中,打开 starter 项目。在本教程的第一部分,你将完全在 MainViewController.swift 内工作,学习一些核心概念。
在 MainViewController.swift 的底部添加以下类:
swift
/// 用户
class User {
let name: String
init(name: String) {
self.name = name
print("User \(name) was initialized")
}
deinit {
print("Deallocating user named: \(name)")
}
}
这里定义了一个 User 类,它用 print 语句来显示你初始化或删除它的时间点。
现在,在 MainViewController 的顶部初始化一个 User 实例。
把下面的代码放在 viewDidLoad() 上面:
swift
class MainViewController: UIViewController {
let user = User(name: "John")
override func viewDidLoad() {
super.viewDidLoad()
}
}
构建并运行该应用程序。用 Command-Shift-Y 确保控制台可见,这样你就可以看到 print 语句的结果。
注意控制台显示 User John was initialized ,deinit 内的 print 语句从未被调用。这意味着该对象从未被取消分配,因为它从未超出作用域范围。
换句话说,因为包含这个对象的视图控制器永远不会超出范围(goes out of scope),所以这个对象永远不会从内存中删除。
💡 这里的
user实例作为MainViewController视图控制器中的一个存储属性 存在,因此它在MainViewController视图控制器的整个生命周期内都会"存活",所以不会被 ARC 取消分配并释放内存。
这是在范围内吗?
将 user 实例包裹在一个方法中,可以让它走出作用域范围,让 ARC 去取消分配它。
在 MainViewController 类中创建一个名为 runScenario() 的方法。把 User 的初始化移到它里面:
swift
func runScenario() {
let user = User(name: "John")
}
runScenario() 定义了 User 实例的范围。在这个作用域的末尾,User 应该被取消分配。
现在,通过在 viewDidLoad() 的末尾添加以下内容来调用 runScenario():
swift
override func viewDidLoad() {
super.viewDidLoad()
runScenario()
}
构建并再次运行。现在的控制台输出看起来像这样:
sql
User John was initialized
Deallocating user named: John
初始化和取消分配的 print 语句都出现了。这些语句表明你已经在作用域的末端对对象进行了取消分配。
对象的生命周期
一个 Swift 对象的生命周期由五个阶段组成:
- 分配(Allocation):从堆或堆中获取内存。
- 初始化(Initialization) :运行
init代码。 - 使用。
- 反初始化(Deinitialization) :运行
deinit代码。 - **取消分配(Deallocation):**将内存返回到堆或堆中。
对分配和取消分配没有直接的钩子,但你可以使用 init 和 deinit 中的 print 语句作为监控这些进程的代理。
引用计数(Reference counts) ,也被称为使用计数(usage counts),确定一个对象何时不再需要。这个计数表明有多少 "东西" 引用该对象。当对象的使用计数达到零,并且该对象的拥有者不存在时,该对象就不再需要了。然后,该对象将被反初始化和取消分配。

当你初始化 User 对象时,它以一个引用计数开始,因为常数 user 引用了该对象。
在 runScenario() 结束时, user 离开了作用域,引用计数减少到了 0。结果是,user 反初始化并在随后取消分配。
引用循环
在大多数情况下,ARC 工作得很好。作为一个应用程序的开发者,你通常不必担心内存泄漏,即"未使用的对象会无限期地存在"的情况。
但也并非一帆风顺。泄漏可能发生!
这些泄漏是如何发生的呢?想象一下这样的情况:两个对象不再需要了,但每个对象都引用另一个对象(两个对象互相引用对方)。由于每个对象都有一个非零的引用计数,所以这两个对象都不能取消分配。

这是一个强引用循环(strong reference cycle)。它愚弄了 ARC,使其无法正常清理内存。
正如你所看到的,最后的引用计数并不是零,而且即使两者都不需要,object1 和 object2 也没有被取消分配。
检查你的引用
要看到这个动作,请在 MainViewController.swift 的 User 后面添加以下代码:
swift
/// 手机
class Phone {
let model: String
var owner: User?
init(model: String) {
self.model = model
print("Phone \(model) was initialized")
}
deinit {
print("Deallocating phone named: \(model)")
}
}
这增加了一个名为 Phone 的新类。它有两个属性,model 描述手机型号名称,owner 描述手机的拥有者,有 init 和 deinit 方法。 owner 属性是可选的,因为一个 Phone 可以在没有用户的情况下存在。
接下来在 runScenario() 中添加以下一行:
swift
func runScenario() {
let user = User(name: "John")
let iPhone = Phone(model: "iPhone Xs")
}
这将创建一个 Phone 的实例。
拥有 Phone
接下来,在 User 中添加以下代码,紧接在 name 属性之后:
swift
private(set) var phones: [Phone] = [] // User.phones -> Phone
func add(phone: Phone) {
phones.append(phone)
phone.owner = self // Phone.owner -> User
}
这增加了一个 phones 数组属性,以保存用户拥有的所有手机。setter 是私有的,所以用户必须使用 add(phone:)。这个方法确保在你添加时正确设置手机的拥有者(owner)。
构建并运行。正如你在控制台中看到的,手机和用户对象如预期的那样被取消了分配:
sql
User John was initialized
Phone iPhone Xs was initialized
Deallocating phone named: iPhone Xs
Deallocating user named: John
现在,在 runScenario() 的末尾添加以下内容。:
swift
user.add(phone: iPhone)
add(phone:) 也将 iPhone 的所有者属性设置为 user。
现在构建并运行,你会看到 user 和 iPhone 并没有被取消分配。这两个对象之间的强引用循环阻止了 ARC 对它们中的任何一个取消分配(内存)。

弱引用(weak)
为了打破强引用循环,你可以指定引用计数对象之间的关系为 weak 引用。
除非另有规定,默认所有的引用都是强引用,并影响引用计数。然而,弱引用不会增加一个对象的引用计数。
换句话说,**弱引用并不参与对象的生命周期管理。**此外,弱引用总是被声明为可选类型 。这意味着当引用计数为零时,引用可以自动被设置为 nil。

在上面的图片中,虚线箭头代表一个弱引用。请注意 object1 的引用计数是 1,因为 variable1 引用了它。object2 的引用计数是 2,因为 variable2 和 object1 都引用了它。
虽然 object2 引用了 object1,但它是弱引用,意味着它不影响 object1 的引用计数。
当 variable1 和 variable2 都消失时,object1 的引用计数将为零,deinit 将运行。这就删除了对 object2 的强引用,随后 object2 就会被反初始化。
回到 Phone 类中,改变 owner 声明以匹配以下内容:
swift
// 声明为弱引用,不增加引用计数,打破引用循环
// 弱引用始终是可选(optional)的,并且当被引用的对象消失时会自动变为 nil。所以你必须将弱属性定义为可选的 var 类型
weak var owner: User?
使 owner 的引用变为 weak 弱引用,打破了 User 到 Phone 的引用循环。

构建并再次运行。现在,一旦 runScenario() 方法退出范围,user 和 iPhone 就会正确地取消分配。
无主引用(unowned)
还有一个可以使用的不增加引用次数的引用修饰语:unowned。
unowned 和 weak 之间有什么区别?弱引用始终是可选(optional)的,并且当被引用的对象消失时会自动变为 nil。
这就是为什么你必须把弱引用定义为可选的 var 类型,这样你的代码才能被编译。该属性需要改变。
相比之下,unowned 无主引用绝不是可选类型。如果你尝试访问引用已经反初始化的无主引用属性,你将触发运行时错误,类似于强制解包 nil 可选类型。

是时候用 unowned 的方式来练习一下了。
在 MainViewController.swift 的末尾添加一个新类 CarrierSubscription。
swift
/// 运营商订阅
class CarrierSubscription {
let name: String
let countryCode: String
let number: String
let user: User // CarrierSubscription.user -> User
init(name: String, countryCode: String, number: String, user: User) {
self.name = name
self.countryCode = countryCode
self.number = number
self.user = user
print("CarrierSubscription \(name) is initialized")
}
deinit {
print("Deallocating CarrierSubscription named: \(name)")
}
}
CarrierSubscription 有四个属性:
Name: 订阅的名称。CountryCode: 订阅的国家。number: 电话号码。user: 对User对象的引用。
谁是你的运营商?
接下来,在 name 属性后面给 User 添加以下内容:
swift
var subscriptions: [CarrierSubscription] = [] // User.subscriptions -> CarrierSubscription
这增加了一个 subscriptions 属性,它持有一个包含 CarrierSubscription 对象的数组。
另外,在 Phone 类的顶部,在 owner 属性下面添加以下内容:
swift
// Phone.carrierSubscription -> CarrierSubscription
var carrierSubscription: CarrierSubscription?
func provision(carrierSubscription: CarrierSubscription) {
self.carrierSubscription = carrierSubscription
}
func decommission() {
carrierSubscription = nil
}
这增加了一个可选的 CarrierSubscription 属性和两个新的方法来提供和退出手机上的运营商订阅。
接下来,在 CarrierSubscription 内部的 init 中添加以下内容,就在 print 语句之前:
swift
user.subscriptions.append(self)
这将 CarrierSubscription 添加到用户的订阅数组中。
最后,在 runScenario() 的末尾添加以下内容:
swift
let subscription = CarrierSubscription(name: "TelBel", countryCode: "0032", number: "31415926", user: user)
iPhone.provision(carrierSubscription: subscription)
这将为 user 创建一个 CarrierSubscription,并为 iPhone 提供它。
构建并运行。注意控制台的打印结果。
csharp
User John was initialized
Phone iPhone Xs was initialized
CarrierSubscription TelBel is initialized
再次,你看到了引用循环。无论是 user、iPhone 还是 subscription,在最后都没有被取消分配。
你能找到现在的问题所在吗?

打破引用链
无论是从 user 到 subscription 的引用还是从 subscription 到 user 的引用都应该是 unowned 的,以打破循环。问题是,在这两者中选择哪一个。这就需要你对特定的业务需求足够了解。
用户拥有运营商的订阅,但是,与运营商可能认为的相反,运营商订阅并不拥有用户。
此外,CarrierSubscription 在没有拥有用户的情况下存在是不合理的。这就是为什么你一开始就把它声明为一个不可变的 let 属性。
由于 User 没有 CarrierSubscription 依然可以存在,但 CarrierSubscription 没有 User 却不能存在,所以它对 user 的引用应该是 unowned 的。
将 CarrierSubscription 中的 user 声明改为如下:
swift
unowned let user: User
user 现在是 unowned 的,打破了引用循环,允许每个对象 deallocate。编译并运行以确认。

使用闭包的引用循环
当属性之间相互引用时,对象的引用循环就会发生。像对象一样,闭包也是引用类型,也会导致引用循环。闭包**捕获(capture)**或关闭它们所操作的对象。
例如,如果你将一个闭包分配给一个类的属性,而该闭包使用同一个类的实例属性,你就会触发引用循环 。换句话说,对象通过一个存储属性持有对闭包的引用。而闭包通过 self 的捕获值持有对对象的引用。

在 CarrierSubscription 中添加以下内容,就在 user 属性的后面:
swift
lazy var completePhoneNumber: () -> String = {
self.countryCode + " " + self.number
}
这个闭包计算并返回一个完整的电话号码。该属性是 lazy 的,这意味着你将把它的赋值推迟到你第一次使用该属性时。
这是必要的,因为它使用了 self.countryCode 和 self.number,它们在初始化器运行后才可用。
在 runScenario() 的末尾添加以下一行:
swift
func runScenario() {
let user = User(name: "John")
let iPhone = Phone(model: "iPhone Xs")
user.add(phone: iPhone)
let subscription = CarrierSubscription(name: "TelBel", countryCode: "0032", number: "31415926", user: user)
iPhone.provision(carrierSubscription: subscription)
print(subscription.completePhoneNumber())
}
访问 completePhoneNumber() 将迫使闭包运行并赋值该属性。
编译并运行,你会注意到 user 和 iPhone 会取消分配,但 CarrierSubscription 不会,这是由于对象和闭包之间的强引用循环。

捕获列表(Capture Lists)
Swift 有一种简单、优雅的方式来打破闭包中的强引用循环。你可以声明一个捕获列表,在其中定义闭包和它捕获的对象之间的关系。
为了说明捕获列表是如何工作的,请看下面的代码:
swift
var x = 5
var y = 5
let someClosure = { [x] in
print("\(x), \(y)")
}
x = 6
y = 6
someClosure() // Prints 5, 6
print("\(x), \(y)") // Prints 6, 6
x 在闭包捕获列表中,所以你在闭包的定义点复制了 x。它是由值捕获的。
y 不在捕获列表中,而是通过引用来捕获 。这意味着当闭包运行时,y 将是任何东西,而不是它在捕获点的样子。
捕获列表在定义闭包中使用的对象之间的 weak 或 unowned 时非常方便。在这种情况下, unowned 是一个很好的选择,因为如果 CarrierSubscription 的实例已经消失了,那么闭包就不可能存在。
捕获 self
将 CarrierSubscription 中的 completePhoneNumber 的声明替换为以下内容:
swift
// 将 [unowned self] 添加到闭包捕获列表中
// [unowned self] 的完整语法 [unowned newID = self]
// 如果你可以确定闭包中的引用对象永远不会释放,则可以使用 unowned。
lazy var completePhoneNumber: () -> String = { [unowned self] in
self.countryCode + " " + self.number
}
这将 [unowned self] 添加到闭包的捕获列表中。这意味着你将 self 作为一个 unowned 无主引用而不是一个强引用来捕获。
构建并运行,你会看到 CarrierSubscription 现在被删除了。这就解决了引用循环问题。万幸!
这里使用的语法实际上是一个较长的捕获语法的速记,它引入了一个新的标识符。考虑一下较长的形式:
swift
var closure = { [unowned newID = self] in
// Use unowned newID here...
}
这里,newID 是 self 的一个 unowned 副本。在闭包的作用域之外,self 保持其原有的含义。在上面使用的简短形式中,你正在创建一个新的 self 变量,它只在闭包的作用域内对现有的 self 变量进行影射。
小心使用 unowned
在你的代码中,self 和 completePhoneNumber 之间的关系是 unowned。
如果你确信一个来自闭包的引用对象永远不会取消分配,你可以使用 unowned。然而,如果它真的取消分配了,你就有麻烦了。
在 MainViewController.swift 的末尾添加以下代码:
swift
class WWDCGreeting {
let who: String
init(who: String) {
self.who = who
}
lazy var greetingMaker: () -> String = { [unowned self] in
return "Hello \(self.who)"
}
}
接下来,在 runScenario() 的末尾添加以下代码块:
swift
let greetingMaker: () -> String // 把闭包声明为属性保存到 do{} 语句外
do {
let mermaid = WWDCGreeting(who: "caffeinated mermaid")
greetingMaker = mermaid.greetingMaker
} // do 语句结束时,mermaid 引用的对象已经被释放。
// 这里再次尝试在闭包中访问 self 所指向的 mermaid 对象,会引发运行时异常。
print(greetingMaker()) // TRAP!
构建并运行,你会在控制台中出现类似以下的崩溃信息:
vbnet
User John was initialized
Phone iPhone Xs was initialized
CarrierSubscription TelBel is initialized
0032 31415926
Fatal error: Attempted to read an unowned reference but object 0x2837e5530 was already deallocated2023-02-25 10:41:04.250687+0800
Cycles[26291:18330770] Fatal error: Attempted to read an unowned reference but object 0x2837e5530 was already deallocated
应用程序遇到了一个运行时异常,因为闭包期望 self.who 仍然有效,但是当 mermaid 在 do 块的末尾超出作用域范围时,你把它删除了。
这个例子可能看起来很牵强,但它在现实生活中会发生。一个例子是,当你使用闭包来运行一些更晚的东西时,比如在一个异步网络调用完成后。
解除陷阱
用以下内容替换 WWDCGreeting 中的 greetingMaker 变量:
swift
lazy var greetingMaker: () -> String = { [weak self] in
return "Hello \(self?.who)."
}
在这里,你对原始的 greetingMaker 做了两个改动。首先,你用 weak 代替了 unowned。第二,由于 self 变成了弱引用,你需要用 self?.who 来访问 who 属性。你可以忽略 Xcode 的警告;你很快就会修复它。
应用程序不再崩溃,但当你构建和运行时,你在控制台得到一个奇怪的结果。"Hello nil."
现在做一些不同的事情
也许这个结果在这种情况下可以接受,但更多时候,当对象不存在时,你会想做一些完全不同的事情。Swift 的 guard let 让这一切变得简单。
最后一次用下面的语句替换闭包:
swift
lazy var greetingMaker: () -> String = { [weak self] in
guard let self = self else {
return "No greeting available."
}
return "Hello \(self.who)."
}
guard 语句将 weak self 绑定到 self。如果 self 是 nil,闭包会返回 "No greeting available."。
另一方面,如果 self 不是 nil,它会使 self 成为一个强引用,所以对象被保证活到闭包的最后。
这个成语有时被称为强弱之舞(strong-weak dance) ,是 Ray Wenderlich Swift 风格指南的一部分,因为它是处理闭包中这种行为的一种稳健模式。

构建并运行,看你现在得到了相应的消息。
在 Xcode 10 中寻找引用循环
现在你了解了 ARC 的原理,什么是引用循环,以及如何打破它们,现在是时候看看一个真实世界的例子了。
在 Xcode 中打开 Contacts 文件夹中的 Starter 项目。
构建并运行该项目,你会看到以下内容。

这是一个简单的联系人应用程序。随意点击一个联系人以获得更多信息,或使用屏幕右上方的 + 按钮添加联系人。
请看一下代码:
ContactsTableViewController:显示数据库中所有的联系人对象。DetailViewController:显示某个Contact对象的详细信息。NewContactViewController:允许用户添加一个新的联系人。ContactTableViewCell:一个自定义的列表视图单元格,显示一个Contact对象的详细信息。Contact:数据库中联系人的模型。Number***:***一个电话号码的模型。
然而,这个项目有一些可怕的错误。埋藏在那里的是一个引用循环。你的用户在相当长的一段时间内不会注意到这个问题,因为泄漏的对象很小,它们的大小使得泄漏更难追踪。
幸运的是,Xcode 10 有一个内置的工具来帮助你找到最小的泄漏。
再次构建并运行该应用程序。通过将他们的单元格向左滑动并点击删除,删除三或四个联系人。他们似乎已经完全消失了,对吗?

泄漏点在哪里?
当应用程序仍在运行时,移动到 Xcode 的底部并点击 Debug Memory Graph 按钮。

观察左侧 Debug 导航栏中的 Runtime Issues。它们被标记为紫色的方块,里面有白色的感叹号,如本截图中选择的那个:

在 navigator 中,选择一个有问题的 Contact 对象。这个循环是清晰可见的。Contact 和 Number 对象通过互相引用来保持彼此存活。

这些问题是一个信号,让你去研究你的代码。考虑到一个 Contact 可以在没有 Number 的情况下存在,但一个 Number 不应该在没有 Contact 的情况下存在。
你将如何解决这个循环?从 Contact 到 Number 的引用或者从 Number 到 Contact 的引用应该是 weak 的还是 unowned 的?
先尽力而为吧,如果你需要帮助的话,请看下面的答案:
swift
class Number {
unowned var contact: Contact
// Other code...
}
swift
class Contact {
var number: Number?
// Other code...
}
奖金:值类型和引用类型的循环
Swift 类型是引用类型(reference types) ,如 class 类;或者值类型(value types) ,如 struct 或 enum。当你传递一个值类型时,你会复制它,而引用类型则共享它们所引用信息的单一副本。
这意味着你不能用值类型进行循环。所有与值类型有关的东西都是一个拷贝,而不是一个引用,这意味着它们不能创建循环关系。你至少需要两个引用类型才能形成一个循环。
回到 Cycles 项目中,在 MainViewController.swift 的末尾添加以下内容:
swift
struct Node { // Error
var payload = 0
var next: Node?
}
嗯,编译器不高兴了。一个 struct 类型不能是递归的,也不能使用自己的实例。否则,这种类型的 struct 会有无限大。
把 struct 改为类:
swift
class Node {
var payload = 0
var next: Node?
}
对于类(即引用类型)来说,自引用不是一个问题,所以编译器错误消失了。
现在,在 MainViewController.swift 的结尾处加上这个:
swift
class Person {
var name: String
var friends: [Person] = []
init(name: String) {
self.name = name
print("New person instance: \(name)")
}
deinit {
print("Person instance \(name) is being deallocated")
}
}
并在 runScenario() 的末尾加上这句话:
swift
do {
let ernie = Person(name: "Ernie")
let bert = Person(name: "Bert")
ernie.friends.append(bert) // Not deallocated
bert.friends.append(ernie) // Not deallocated
}
构建并运行。注意,Bert 和 Ernie 都没有被解除分配内存。
引用和值
这是一个值类型和引用类型混合的例子,形成了一个引用循环。
ernie 和 bert 通过他们的 friends 数组保持对对方的引用而保持存活,尽管数组本身是一个值类型。
设置 friends 数组为 unowned,Xcode 会显示一个错误:unowned 只适用于 class 类型。
为了打破这个循环,你必须创建一个泛型包装对象,并使用它来添加实例到数组中。如果你不知道什么是泛型或如何使用它们,请查看本网站的泛型介绍教程。
在 Person 类的定义上面添加以下内容:
swift
class Unowned<T: AnyObject> {
unowned var value: T
init(_ value: T) {
self.value = value
}
}
然后,像这样修改 Person 中 friends 的定义:
swift
var friends: [Unowned<Person>] = []
最后,将 runScenario() 中的 do 块替换为以下内容:
swift
do {
let ernie = Person(name: "Ernie")
let bert = Person(name: "Bert")
ernie.friends.append(Unowned(bert))
bert.friends.append(Unowned(bert))
}
构建并运行。ernie 和 bert 现在可以愉快地取消分配了!
friends 数组不再是 Person 对象的集合,而是 Unowned 对象的集合,作为 Person 实例的包装器。
要访问 Unowned 中的 Person 对象,需要使用 value 属性,像这样:
swift
let firstFriend = bert.friends.first?.value
何去何从?
你可以使用本教程顶部或底部的下载材料按钮下载项目的完整版本。
你现在对 Swift 中的内存管理有了很好的了解,并且知道 ARC 是如何工作的。如果你想了解更多关于 Xcode 10 中的调试工具,请观看这个 WWDC 会议或查看 iOS 10 by Tutorials Chapter 2。
如果你想更深入地了解 Swift 中的弱引用实现,请查看 Mike Ash 的博文 Swift 弱引用。它涵盖了 Swift 中的弱引用与 Objective-C 的实现有什么不同,以及 Swift 实际上是如何在引擎盖下保持两个引用计数的。一个是强引用,一个是弱引用。
最后,如果你是 raywenderlich.com 的订阅者,请查看 iOS 10 截屏。内存图调试器。这些教程给出了一些很好的提示,可以让你最大限度地利用内存可视化器。
你对 ARC 的方法有什么看法?请在评论中分享你的想法!
附录:本文源码
swift
import UIKit
class MainViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
runScenario()
}
func runScenario() {
let user = User(name: "John")
let iPhone = Phone(model: "iPhone Xs")
user.add(phone: iPhone)
let subscription = CarrierSubscription(name: "TelBel", countryCode: "0032", number: "31415926", user: user)
iPhone.provision(carrierSubscription: subscription)
print(subscription.completePhoneNumber())
// MARK: - 小心使用 unowned
let greetingMaker: () -> String
do {
let mermaid = WWDCGreeting(who: "caffeinated mermaid")
greetingMaker = mermaid.greetingMaker
} // do 语句结束时,mermaid 引用的对象已经被释放。
// 这里再次尝试在闭包中访问 self 所指向的 mermaid 对象,会引发运行时异常。
print(greetingMaker()) // TRAP!
// MARK: - 奖金:值类型和引用类型的循环
do {
let ernie = Person(name: "Ernie")
let bert = Person(name: "Bert")
ernie.friends.append(Unowned(bert))
bert.friends.append(Unowned(bert))
// 需要使用 value 属性访问 Unowned 中的 Person 对象
let firstFriend = bert.friends.first?.value
}
}
}
/// 用户
class User {
let name: String
private(set) var phones: [Phone] = []
var subscriptions: [CarrierSubscription] = []
init(name: String) {
self.name = name
print("User \(name) was initialized")
}
func add(phone: Phone) {
phones.append(phone)
phone.owner = self
}
deinit {
print("Deallocating user named: \(name)")
}
}
/// 手机
class Phone {
let model: String
// 声明为弱引用,不增加引用计数,打破引用循环
// 弱引用始终是可选(optional)的,并且当被引用的对象消失时会自动变为 nil。所以你必须将弱属性定义为可选的 var 类型
weak var owner: User?
var carrierSubscription: CarrierSubscription?
init(model: String) {
self.model = model
print("Phone \(model) was initialized")
}
// 在手机上配置运营商
func provision(carrierSubscription: CarrierSubscription) {
self.carrierSubscription = carrierSubscription
}
// 停用运营商
func decommission() {
carrierSubscription = nil
}
deinit {
print("Deallocating phone named: \(model)")
}
}
/// 运营商订阅
class CarrierSubscription {
let name: String
let countryCode: String
let number: String
// 没有用户的运营商订阅没有存在的意义,因此这里设置为无主引用
unowned let user: User
// 将 [unowned self] 添加到闭包捕获列表中
// [unowned self] 的完整语法 [unowned newID = self]
// 如果你可以确定闭包中的引用对象永远不会释放,则可以使用 unowned。
lazy var completePhoneNumber: () -> String = { [unowned self] in
self.countryCode + " " + self.number
}
init(name: String, countryCode: String, number: String, user: User) {
self.name = name
self.countryCode = countryCode
self.number = number
self.user = user
user.subscriptions.append(self)
print("CarrierSubscription \(name) is initialized")
}
deinit {
print("Deallocating CarrierSubscription named: \(name)")
}
}
// MARK: - 小心使用 unowned
class WWDCGreeting {
let who: String
init(who: String) {
self.who = who
}
/// <https://github.com/raywenderlich/swift-style-guide#memory-management>
lazy var greetingMaker: () -> String = { [weak self] in
guard let self = self else {
return "No greeting available."
}
return "Hello \(self.who)."
}
}
// MARK: - 奖金:值类型和引用类型的循环
class Node {
var payload = 0
var next: Node?
}
// 解决方案:通过 Generics 泛型包装器打破引用循环问题
class Unowned<T: AnyObject> {
unowned var value: T
init(_ value: T) {
self.value = value
}
}
class Person {
var name: String
// 你不能用 unowned 修饰数组,因为 unowned 只适用于 class 类型,而数组是个值类型。
// var friends: [Person] = []
var friends: [Unowned<Person>] = []
init(name: String) {
self.name = name
print("New person instance: \(name)")
}
deinit {
print("Person instance \(name) is being deallocated")
}
}