原文: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")
}
}