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

  1. 分配(Allocation):从堆或堆中获取内存。
  2. 初始化(Initialization) :运行 init 代码。
  3. 使用。
  4. 反初始化(Deinitialization) :运行 deinit 代码。
  5. **取消分配(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")
    }
}
相关推荐
Java后端的Ai之路5 小时前
06-React-Props
前端·react.js·前端框架
Flynt6 小时前
开源"间谍卫星"说数据全是真的,我把它的免key数据源挨个试了一遍
前端·开源·数据可视化
Csvn6 小时前
渲染管线
前端
程序员大阳6 小时前
副队长CSS教程(33)–圆角与透明
前端·css·透明·圆角
志尊宝6 小时前
Vue3 零基础每日笔记(082):文件上传实战——el-upload 校验、自定义上传、头像预览
前端·javascript·vue.js·笔记·#javascript·#vue.js
鬼手点金7 小时前
opencode-rules 插件
java·开发语言·前端·人工智能·chrome·git·github
柳杉7 小时前
借助 Codex 和可视化工作流,从调研到完整实现,3D 大屏竟然可以这么简单
前端·ai编程·数据可视化
IMPYLH7 小时前
HTML 的 <wbr> 元素
前端·html
Dovis(誓平步青云)8 小时前
常用命令藏得太深?怎么做一个快捷命令坞
运维·前端·自动化·音视频·prometheus
IT_陈寒9 小时前
Vite的这个问题坑了我三天,原来如此解决!
前端·人工智能·后端