Swift 中的 ARC 和内存管理

原文: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 initializeddeinit 内的 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 对象的生命周期由五个阶段组成:

  1. 分配(Allocation):从堆或堆中获取内存。
  2. 初始化(Initialization) :运行 init 代码。
  3. 使用。
  4. 反初始化(Deinitialization) :运行 deinit 代码。
  5. **取消分配(Deallocation):**将内存返回到堆或堆中。

对分配和取消分配没有直接的钩子,但你可以使用 initdeinit 中的 print 语句作为监控这些进程的代理。

引用计数(Reference counts) ,也被称为使用计数(usage counts),确定一个对象何时不再需要。这个计数表明有多少 "东西" 引用该对象。当对象的使用计数达到零,并且该对象的拥有者不存在时,该对象就不再需要了。然后,该对象将被反初始化和取消分配。

当你初始化 User 对象时,它以一个引用计数开始,因为常数 user 引用了该对象。

runScenario() 结束时, user 离开了作用域,引用计数减少到了 0。结果是,user 反初始化并在随后取消分配。

引用循环

在大多数情况下,ARC 工作得很好。作为一个应用程序的开发者,你通常不必担心内存泄漏,即"未使用的对象会无限期地存在"的情况。

但也并非一帆风顺。泄漏可能发生!

这些泄漏是如何发生的呢?想象一下这样的情况:两个对象不再需要了,但每个对象都引用另一个对象(两个对象互相引用对方)。由于每个对象都有一个非零的引用计数,所以这两个对象都不能取消分配。

这是一个强引用循环(strong reference cycle)。它愚弄了 ARC,使其无法正常清理内存。

正如你所看到的,最后的引用计数并不是零,而且即使两者都不需要,object1object2 也没有被取消分配。

检查你的引用

要看到这个动作,请在 MainViewController.swiftUser 后面添加以下代码:

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 描述手机的拥有者,有 initdeinit 方法。 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

现在构建并运行,你会看到 useriPhone 并没有被取消分配。这两个对象之间的强引用循环阻止了 ARC 对它们中的任何一个取消分配(内存)。

弱引用(weak)

为了打破强引用循环,你可以指定引用计数对象之间的关系为 weak 引用。

除非另有规定,默认所有的引用都是强引用,并影响引用计数。然而,弱引用不会增加一个对象的引用计数。

换句话说,**弱引用并不参与对象的生命周期管理。**此外,弱引用总是被声明为可选类型 。这意味着当引用计数为零时,引用可以自动被设置为 nil

在上面的图片中,虚线箭头代表一个弱引用。请注意 object1 的引用计数是 1,因为 variable1 引用了它。object2 的引用计数是 2,因为 variable2object1 都引用了它。

虽然 object2 引用了 object1,但它是弱引用,意味着它不影响 object1 的引用计数。

variable1variable2 都消失时,object1 的引用计数将为零,deinit 将运行。这就删除了对 object2 的强引用,随后 object2 就会被反初始化。

回到 Phone 类中,改变 owner 声明以匹配以下内容:

swift 复制代码
// 声明为弱引用,不增加引用计数,打破引用循环
// 弱引用始终是可选(optional)的,并且当被引用的对象消失时会自动变为 nil。所以你必须将弱属性定义为可选的 var 类型
weak var owner: User?

使 owner 的引用变为 weak 弱引用,打破了 UserPhone 的引用循环。

构建并再次运行。现在,一旦 runScenario() 方法退出范围,useriPhone 就会正确地取消分配。

无主引用(unowned)

还有一个可以使用的不增加引用次数的引用修饰语:unowned

unownedweak 之间有什么区别?弱引用始终是可选(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

再次,你看到了引用循环。无论是 useriPhone 还是 subscription,在最后都没有被取消分配。

你能找到现在的问题所在吗?

打破引用链

无论是从 usersubscription 的引用还是从 subscriptionuser 的引用都应该是 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.countryCodeself.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() 将迫使闭包运行并赋值该属性。

编译并运行,你会注意到 useriPhone 会取消分配,但 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 将是任何东西,而不是它在捕获点的样子。

捕获列表在定义闭包中使用的对象之间的 weakunowned 时非常方便。在这种情况下, 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...
}

这里,newIDself 的一个 unowned 副本。在闭包的作用域之外,self 保持其原有的含义。在上面使用的简短形式中,你正在创建一个新的 self 变量,它只在闭包的作用域内对现有的 self 变量进行影射。

小心使用 unowned

在你的代码中,selfcompletePhoneNumber 之间的关系是 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 仍然有效,但是当 mermaiddo 块的末尾超出作用域范围时,你把它删除了。

这个例子可能看起来很牵强,但它在现实生活中会发生。一个例子是,当你使用闭包来运行一些更晚的东西时,比如在一个异步网络调用完成后

解除陷阱

用以下内容替换 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。如果 selfnil,闭包会返回 "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 对象。这个循环是清晰可见的。ContactNumber 对象通过互相引用来保持彼此存活。

这些问题是一个信号,让你去研究你的代码。考虑到一个 Contact 可以在没有 Number 的情况下存在,但一个 Number 不应该在没有 Contact 的情况下存在。

你将如何解决这个循环?从 ContactNumber 的引用或者从 NumberContact 的引用应该是 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) ,如 structenum。当你传递一个值类型时,你会复制它,而引用类型则共享它们所引用信息的单一副本。

这意味着你不能用值类型进行循环。所有与值类型有关的东西都是一个拷贝,而不是一个引用,这意味着它们不能创建循环关系。你至少需要两个引用类型才能形成一个循环。

回到 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
}

构建并运行。注意,BertErnie 都没有被解除分配内存。

引用和值

这是一个值类型和引用类型混合的例子,形成了一个引用循环。

erniebert 通过他们的 friends 数组保持对对方的引用而保持存活,尽管数组本身是一个值类型

设置 friends 数组为 unowned,Xcode 会显示一个错误:unowned 只适用于 class 类型。

为了打破这个循环,你必须创建一个泛型包装对象,并使用它来添加实例到数组中。如果你不知道什么是泛型或如何使用它们,请查看本网站的泛型介绍教程

Person 类的定义上面添加以下内容:

swift 复制代码
class Unowned<T: AnyObject> {
    unowned var value: T
    init(_ value: T) {
        self.value = value
    }
}

然后,像这样修改 Personfriends 的定义:

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))
}

构建并运行。erniebert 现在可以愉快地取消分配了!

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")
    }
}
相关推荐
万叶学编程3 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js
前端李易安4 小时前
Web常见的攻击方式及防御方法
前端
PythonFun5 小时前
Python技巧:如何避免数据输入类型错误
前端·python
知否技术5 小时前
为什么nodejs成为后端开发者的新宠?
前端·后端·node.js
hakesashou5 小时前
python交互式命令时如何清除
java·前端·python
天涯学馆5 小时前
Next.js与NextAuth:身份验证实践
前端·javascript·next.js
HEX9CF5 小时前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
ConardLi5 小时前
Chrome:新的滚动捕捉事件助你实现更丝滑的动画效果!
前端·javascript·浏览器
ConardLi5 小时前
安全赋值运算符,新的 JavaScript 提案让你告别 trycatch !
前端·javascript