原文: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
的底部增加以下类:
/// 用户
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()
上面:
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
的初始化移到它里面:
func runScenario() {
let user = User(name: "John")
}
runScenario()
界说了 User
实例的规模。在这个效果域的结尾,User
应该被撤销分配。
现在,经过在 viewDidLoad()
的结尾增加以下内容来调用 runScenario()
:
override func viewDidLoad() {
super.viewDidLoad()
runScenario()
}
构建并再次运转。现在的操控台输出看起来像这样:
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
后面增加以下代码:
/// 手机
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()
中增加以下一行:
func runScenario() {
let user = User(name: "John")
let iPhone = Phone(model: "iPhone Xs")
}
这将创立一个 Phone
的实例。
具有 Phone
接下来,在 User
中增加以下代码,紧接在 name
特点之后:
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
)。
构建并运转。正如你在操控台中看到的,手机和用户目标如预期的那样被撤销了分配:
User John was initialized
Phone iPhone Xs was initialized
Deallocating phone named: iPhone Xs
Deallocating user named: John
现在,在 runScenario()
的结尾增加以下内容。:
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
声明以匹配以下内容:
// 声明为弱引证,不增加引证计数,打破引证循环
// 弱引证始终是可选(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
。
/// 运营商订阅
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
增加以下内容:
var subscriptions: [CarrierSubscription] = [] // User.subscriptions -> CarrierSubscription
这增加了一个 subscriptions
特点,它持有一个包括 CarrierSubscription
目标的数组。
别的,在 Phone
类的顶部,在 owner
特点下面增加以下内容:
// Phone.carrierSubscription -> CarrierSubscription
var carrierSubscription: CarrierSubscription?
func provision(carrierSubscription: CarrierSubscription) {
self.carrierSubscription = carrierSubscription
}
func decommission() {
carrierSubscription = nil
}
这增加了一个可选的 CarrierSubscription
特点和两个新的办法来供给和退出手机上的运营商订阅。
接下来,在 CarrierSubscription
内部的 init
中增加以下内容,就在 print
句子之前:
user.subscriptions.append(self)
这将 CarrierSubscription
增加到用户的订阅数组中。
最终,在 runScenario()
的结尾增加以下内容:
let subscription = CarrierSubscription(name: "TelBel", countryCode: "0032", number: "31415926", user: user)
iPhone.provision(carrierSubscription: subscription)
这将为 user
创立一个 CarrierSubscription
,并为 iPhone
供给它。
构建并运转。留意操控台的打印结果。
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
声明改为如下:
unowned let user: User
user
现在是 unowned
的,打破了引证循环,允许每个目标 deallocate。编译并运转以确认。
运用闭包的引证循环
当特点之间彼此引证时,目标的引证循环就会产生。像目标相同,闭包也是引证类型,也会导致引证循环。闭包**捕获(capture)**或关闭它们所操作的目标。
例如,假如你将一个闭包分配给一个类的特点,而该闭包运用同一个类的实例特点,你就会触发引证循环。换句话说,目标经过一个存储特点持有对闭包的引证。而闭包经过 self
的捕获值持有对目标的引证。
在 CarrierSubscription
中增加以下内容,就在 user
特点的后面:
lazy var completePhoneNumber: () -> String = {
self.countryCode + " " + self.number
}
这个闭包核算并回来一个完好的电话号码。该特点是 lazy
的,这意味着你将把它的赋值推迟到你第一次运用该特点时。
这是必要的,由于它运用了 self.countryCode
和 self.number
,它们在初始化器运转后才可用。
在 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())
}
拜访 completePhoneNumber()
将迫使闭包运转并赋值该特点。
编译并运转,你会留意到 user
和 iPhone
会撤销分配,但 CarrierSubscription
不会,这是由于目标和闭包之间的强引证循环。
捕获列表(Capture Lists)
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
的声明替换为以下内容:
// 将 [unowned self] 增加到闭包捕获列表中
// [unowned self] 的完好语法 [unowned newID = self]
// 假如你能够确认闭包中的引证目标永久不会开释,则能够运用 unowned。
lazy var completePhoneNumber: () -> String = { [unowned self] in
self.countryCode + " " + self.number
}
这将 [unowned self]
增加到闭包的捕获列表中。这意味着你将 self
作为一个 unowned
无主引证而不是一个强引证来捕获。
构建并运转,你会看到 CarrierSubscription
现在被删去了。这就处理了引证循环问题。万幸!
这儿运用的语法实际上是一个较长的捕获语法的速记,它引入了一个新的标识符。考虑一下较长的方式:
var closure = { [unowned newID = self] in
// Use unowned newID here...
}
这儿,newID
是 self
的一个 unowned
副本。在闭包的效果域之外,self
坚持其原有的含义。在上面运用的简略方式中,你正在创立一个新的 self
变量,它只在闭包的效果域内对现有的 self
变量进行暗射。
当心运用 unowned
在你的代码中,self
和 completePhoneNumber
之间的联系是 unowned
。
假如你确信一个来自闭包的引证目标永久不会撤销分配,你能够运用 unowned
。可是,假如它真的撤销分配了,你就有麻烦了。
在 MainViewController.swift
的结尾增加以下代码:
class WWDCGreeting {
let who: String
init(who: String) {
self.who = who
}
lazy var greetingMaker: () -> String = { [unowned self] in
return "Hello (self.who)"
}
}
接下来,在 runScenario()
的结尾增加以下代码块:
let greetingMaker: () -> String // 把闭包声明为特点保存到 do{} 句子外
do {
let mermaid = WWDCGreeting(who: "caffeinated mermaid")
greetingMaker = mermaid.greetingMaker
} // do 句子完毕时,mermaid 引证的目标现已被开释。
// 这儿再次测验在闭包中拜访 self 所指向的 mermaid 目标,会引发运转时反常。
print(greetingMaker()) // TRAP!
构建并运转,你会在操控台中呈现相似以下的崩溃信息:
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
变量:
lazy var greetingMaker: () -> String = { [weak self] in
return "Hello (self?.who)."
}
在这儿,你对原始的 greetingMaker
做了两个改动。首要,你用 weak
替代了 unowned
。第二,由于 self
变成了弱引证,你需求用 self?.who
来拜访 who
特点。你能够疏忽 Xcode 的警告;你很快就会修正它。
应用程序不再崩溃,但当你构建和运转时,你在操控台得到一个古怪的结果。”Hello nil.”
现在做一些不同的作业
也许这个结果在这种情况下能够接受,但更多时分,当目标不存在时,你会想做一些彻底不同的作业。Swift 的 guard let
让这一切变得简略。
最终一次用下面的句子替换闭包:
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
的?
先尽力而为吧,假如你需求协助的话,请看下面的答案:
class Number {
unowned var contact: Contact
// Other code...
}
class Contact {
var number: Number?
// Other code...
}
奖金:值类型和引证类型的循环
Swift 类型是引证类型(reference types),如 class
类;或许值类型(value types),如 struct
或 enum
。当你传递一个值类型时,你会仿制它,而引证类型则同享它们所引证信息的单一副本。
这意味着你不能用值类型进行循环。所有与值类型有关的东西都是一个拷贝,而不是一个引证,这意味着它们不能创立循环联系。你至少需求两个引证类型才干形成一个循环。
回到 Cycles 项目中,在 MainViewController.swift
的结尾增加以下内容:
struct Node { // Error
var payload = 0
var next: Node?
}
嗯,编译器不高兴了。一个 struct
类型不能是递归的,也不能运用自己的实例。不然,这种类型的 struct
会有无限大。
把 struct
改为类:
class Node {
var payload = 0
var next: Node?
}
对于类(即引证类型)来说,自引证不是一个问题,所以编译器过错消失了。
现在,在 MainViewController.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()
的结尾加上这句话:
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
类的界说上面增加以下内容:
class Unowned<T: AnyObject> {
unowned var value: T
init(_ value: T) {
self.value = value
}
}
然后,像这样修正 Person
中 friends
的界说:
var friends: [Unowned<Person>] = []
最终,将 runScenario()
中的 do
块替换为以下内容:
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
特点,像这样:
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 的办法有什么看法?请在谈论中分享你的想法!
附录:本文源码
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")
}
}