Skip to content

模型化对象及其关系的生命周期

Swift 使用自动引用计数(ARC)来跟踪和管理应用程序的内存使用情况。在大多数情况下,这意味着在 Swift 中内存管理“自动完成”,你不需要自己考虑内存管理。当这些实例不再需要时,ARC 会自动释放这些类实例所使用的内存。

然而,在少数情况下,ARC 需要更多关于代码部分之间关系的信息,以便为您管理内存。本章将描述这些情况,并展示如何使 ARC 能够管理您应用程序的所有内存。在 Swift 中使用 ARC 与使用 Objective-C 的 Transitioning to ARC Release Notes 中描述的方法非常相似。

引用计数仅适用于类的实例。结构体和枚举是值类型,而不是引用类型,不会通过引用存储和传递。

ARC 工作原理

每当创建一个类的新实例时,ARC 会分配一块内存来存储该实例的信息。这块内存包含了实例类型的信息,以及与该实例相关的任何存储属性的值。

此外,当实例不再需要时,ARC 会释放该实例所占用的内存,以便将这些内存用于其他用途。这确保了当实例不再需要时,类实例不会占用内存空间。

然而,如果 ARC 释放了一个仍在使用的实例,就无法再访问该实例的属性,或调用该实例的方法。实际上,如果你尝试访问该实例,你的应用很可能崩溃。

为了确保实例在仍需使用时不会消失,ARC 会跟踪当前引用每个类实例的属性、常量和变量的数量。只要实例还存在至少一个活动引用,ARC 就不会释放该实例。

为了实现这一点,每当将一个类实例分配给一个属性、常量或变量时,该属性、常量或变量就会对该实例进行强引用。这种引用被称为“强”引用,因为它牢牢地保持对该实例的控制,只要该强引用存在,就不会允许该实例被释放。

ARC 在行动

这里有一个自动引用计数工作的示例。这个示例从一个简单的类 Person 开始,该类定义了一个存储常量属性 name

swift
class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

Person 类有一个初始化器,它设置实例的 name 属性并打印一条消息以表示初始化正在进行中。 Person 类还有一个析构器,在类的实例被释放时打印一条消息。

下一个代码片段定义了三个类型为 Person? 的变量,这些变量用于在后续代码片段中设置对新 Person 实例的多个引用。由于这些变量是可选类型( Person? ,而不是 Person ),因此它们会自动初始化为 nil 的值,并且当前不引用任何 Person 实例。

swift
var reference1: Person?
var reference2: Person?
var reference3: Person?

现在你可以创建一个新的 Person 实例并将其赋值给这三个变量之一:

swift
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"

请注意,在调用 Person 类的初始化器时会打印消息 "John Appleseed is being initialized" 。这表明初始化已经完成。

因为新创建的 Person 实例已被赋值给 reference1 变量,所以现在 reference1 到新 Person 实例之间存在一个强引用。由于至少存在一个强引用,ARC 会确保保留这个 Person 并防止其被释放。

如果将同一个 Person 实例赋值给两个或更多的变量,就会建立两个或更多的强引用到该实例:

swift
reference2 = reference1
reference3 = reference1

现在,这个单一的 Person 实例有了三个强引用。

如果通过将 nil 赋值给两个变量来断开其中两个强引用(包括原始引用),那么只剩下一个强引用, Person 实例不会被释放:

swift
reference1 = nil
reference2 = nil

ARC 只有在断开第三个也是最后一个强引用时才会释放 Person 实例,在此之后可以确定你不再使用 Person 实例:

swift
reference3 = nil
// Prints "John Appleseed is being deinitialized"

类实例之间的强引用循环

在上面的示例中,自动引用计数(ARC)能够追踪到您创建的新 Person 实例的引用数量,并在不再需要该 Person 实例时对其进行释放。

然而,有可能编写代码,使得类的实例永远不会达到强引用数量为零的情况。这种情况可能会发生在两个类实例之间持有对方的强引用,使得每个实例都保持对方的存活。这种情况被称为强引用循环

您可以通过将类之间的一些关系定义为弱引用或非拥有所引用,而不是强引用,来解决强引用循环。此过程在“解决类实例之间的强引用循环”中进行了描述。然而,在学习如何解决强引用循环之前,了解这种循环是如何产生的很有用。

这是一个不小心创建强引用循环的示例。此示例定义了两个名为 PersonApartment 的类,它们分别表示一个公寓楼及其居民:

swift
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

Person 实例都有一个 name 属性,类型为 String ,以及一个可选的 apartment 属性,初始值为 nilapartment 属性是可选的,因为一个人可能并不总是有公寓。

同样,每个 Apartment 实例都有一个类型为 unitString 属性,并且有一个可选的 tenant 属性,初始值为 nil 。租户属性是可选的,因为公寓不一定总是有租户。

这两个类还定义了一个析构函数,该函数打印出该类实例正在被析构的事实。这使您可以查看 PersonApartment 的实例是否如预期那样被释放。

下一个代码片段定义了两个名为 johnunit4A 的可选类型变量,稍后将设置为特定的 ApartmentPerson 实例。这两个变量的初始值为 nil ,因为它们是可选的:

swift
var john: Person?
var unit4A: Apartment?

你现在可以创建一个特定的 Person 实例和 Apartment 实例,并将这些新实例分配给 johnunit4A 变量:

swift
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

创建并分配这两个实例之后,强引用如下所示。 john 变量现在有一个对新 Person 实例的强引用,而 unit4A 变量有一个对新 Apartment 实例的强引用:

Strong References

现在可以将这两个实例链接起来,使得人有公寓,公寓有人住。请注意,使用感叹号( ! )来解包并访问存储在 johnunit4A 可选变量中的实例,以便可以设置这些实例的属性:

swift
john!.apartment = unit4A
unit4A!.tenant = john

创建链接这两个实例之后,强引用如下所示:

Strong Reference Cycle

不幸的是,链接这两个实例会在它们之间创建一个强引用循环。 Person 实例现在有一个对 Apartment 实例的强引用,而 Apartment 实例有一个对 Person 实例的强引用。因此,当你打破 johnunit4A 变量持有的强引用时,引用计数不会降到零,ARC 不会释放这些实例:

swift
john = nil
unit4A = nil

请注意,当你将这两个变量设置为 nil 时,它们的析构函数并未被调用。强烈的引用循环阻止了 PersonApartment 实例的任何一次被释放,从而导致你的应用出现内存泄漏。

设置 johnunit4A 变量为 nil 后,强烈的引用关系如下所示:

Memory Leak

Person 实例和 Apartment 实例之间的强烈引用仍然存在且无法被打破。

解决类实例之间的强烈引用循环

Swift 提供了两种解决类类型属性的强引用循环的方法:弱引用未拥有引用

弱引用和未拥有引用使得循环引用中的一个实例能够引用另一个实例而不保持强引用。这样实例之间就可以互相引用而不创建强引用循环。

当另一个实例有较短的生命周期时,即另一个实例可以先被释放时,使用弱引用。例如,在上面的 Apartment 示例中,公寓在某些时候可以没有租户,因此弱引用是打破这种引用循环的合适方式。相反,当另一个实例有相同的生命周期或更长的生命周期时,使用未拥有引用。

弱引用

弱引用不会对它所引用的对象保持强引用,因此不会阻止 ARC 释放被引用的对象。这种行为可以防止引用成为强引用循环的一部分。您可以通过在属性或变量声明前放置 weak 关键字来表示弱引用。

由于弱引用不会对它所引用的对象保持强引用,因此在弱引用仍然引用该对象时,该对象可能会被释放。因此,当弱引用所引用的对象被释放时,ARC 会自动将弱引用设置为 nil 。由于弱引用需要在运行时允许其值更改为 nil ,因此它们总是声明为可选类型的变量,而不是常量。

您可以像检查其他可选值一样检查弱引用中是否存在值,并且永远不会得到一个不再存在的无效实例的引用。

注意

属性观察者在 ARC 将弱引用设置为 nil 时不会被调用。

以下示例与上面的 PersonApartment 示例相同,唯一的区别在于这次 Apartment 类的 tenant 属性被声明为弱引用:

swift
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

与两个变量( johnunit4A )之间的强引用以及两个实例之间的链接相比,这次创建的方式相同:

swift
var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

这是您将两个实例连接在一起后引用的外观:

Weak Reference

Person 实例仍然对 Apartment 实例有强引用,但 Apartment 实例现在对 Person 实例有弱引用。这意味着当您通过将 john 变量设置为 nil 来断开 john 变量持有的强引用时, Person 实例将不再有任何强引用:

swift
john = nil
// Prints "John Appleseed is being deinitialized"

由于不再有对 Person 实例的强引用,因此它被释放,并且 tenant 属性被设置为 nil

Weak Reference Deinitialized

唯一剩余对 Apartment 实例的强引用来自 unit4A 变量。如果你断开这个强引用,就没有对 Apartment 实例的强引用了:

swift
unit4A = nil
// Prints "Apartment 4A is being deinitialized"

由于不再有对 Apartment 实例的强引用,因此它也被释放:

Weak Reference Fully Deinitialized

注意

在使用垃圾回收的系统中,有时会使用弱指针来实现简单的缓存机制,因为没有强引用的对象只有在内存压力触发垃圾回收时才会被释放。然而,在 ARC 中,一旦最后一个强引用被移除,值就会立即被释放,使得弱引用不适合用于这种目的。

未拥有引用

类似于弱引用,未拥有引用不会对它所引用的实例保持强引用。然而,与弱引用不同的是,未拥有引用用于表示其他实例具有相同的生命周期或更长的生命周期。您通过在属性或变量声明前放置 unowned 关键字来表示未拥有引用。

与弱引用不同,未拥有引用总是期望有一个值。因此,将一个值标记为未拥有并不会使其成为可选的,ARC 也不会将未拥有引用的值设置为 nil

重要

只有在您确定引用始终指向一个未被释放的实例时,才使用未拥有引用。

如果尝试在实例被释放后访问未拥有的引用的值,将会得到一个运行时错误。

以下示例定义了两个类 CustomerCreditCard ,它们分别表示银行客户和该客户的可能的信用卡。这两个类中的每一个都存储了另一个类的一个实例作为属性。这种关系有可能创建一个强引用循环。

CustomerCreditCard 之间的关系与上面弱引用示例中 ApartmentPerson 之间的关系略有不同。在这个数据模型中,客户可能没有信用卡,但信用卡总是与客户相关联。 CreditCard 实例永远不会比它所引用的 Customer 实例存在更长时间。为了表示这一点, Customer 类有一个可选的 card 属性,但 CreditCard 类有一个未拥有的(且非可选的) customer 属性。

此外,新的 CreditCard 实例只能通过向自定义 CreditCard 初始化器传递一个 number 值和一个 customer 实例来创建。这确保了当创建 CreditCard 实例时, CreditCard 实例总是与一个 customer 实例相关联。

由于信用卡总是有一个客户,你将其 customer 属性定义为未拥有引用,以避免强引用循环:

swift
class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized") }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}

注意

number 类的 CreditCard 属性定义为 UInt64 类型而不是 Int 类型,以确保 number 属性的容量足够存储 16 位信用卡号码,无论是在 32 位还是 64 位系统上。

下一个代码片段定义了一个可选的 Customer 变量 john ,用于存储特定客户的引用。该变量的初始值为 nil,因为它是一个可选变量:

swift
var john: Customer?

现在你可以创建一个 Customer 实例,并使用它来初始化并分配一个新的 CreditCard 实例作为该客户的 card 属性:

swift
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

现在你已经将两个实例链接起来了,引用看起来是这样的:

Unowned Reference

Customer 实例现在有一个强引用指向 CreditCard 实例,而 CreditCard 实例有一个弱引用指向 Customer 实例。

由于存在弱引用 customer ,当你打破 john 变量持有的强引用时, Customer 实例将不再有任何强引用:

Unowned Reference Deinitialized

既然 Customer 实例不再有任何强引用,它就会被释放。在此之后, CreditCard 实例也没有任何强引用,它也会被释放:

swift
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"

上面的最终代码片段显示, Customer 实例和 CreditCard 实例的析构函数都在 john 变量被设置为 nil 之后打印它们的“析构”消息。

注意

上面的示例展示了如何使用安全未拥有引用。Swift 还提供了不安全未拥有引用,用于需要禁用运行时安全检查的情况——例如,出于性能原因。与所有不安全操作一样,您需要负责检查代码的安全性。

您通过写 unowned(unsafe) 来表示一个不安全未拥有引用。如果您尝试在引用它所指向的实例被释放后访问该不安全未拥有引用,程序将尝试访问实例曾经所在的内存位置,这是一项不安全的操作。

不拥有可选引用

您可以将可选的类引用标记为未拥有。在 ARC 所有权模型中,未拥有可选引用和弱引用可以在相同的上下文中使用。不同之处在于,当您使用未拥有可选引用时,您需要确保它始终指向一个有效的对象或设置为 nil

这是一个跟踪学校中某个系提供的课程的示例:

swift
class Department {
    var name: String
    var courses: [Course]
    init(name: String) {
        self.name = name
        self.courses = []
    }
}

class Course {
    var name: String
    unowned var department: Department
    unowned var nextCourse: Course?
    init(name: String, in department: Department) {
        self.name = name
        self.department = department
        self.nextCourse = nil
    }
}

Department 为每个系提供的课程保持强引用。在 ARC 所有权模型中,系拥有其课程。 Course 有两个未拥有引用,一个指向系,一个指向学生应选的下一门课程;课程不拥有这两个对象。每个课程都是某个系的一部分,因此 department 属性不是可选的。然而,因为有些课程没有推荐的后续课程,所以 nextCourse 属性是可选的。

这是使用这些类的示例:

swift
let department = Department(name: "Horticulture")

let intro = Course(name: "Survey of Plants", in: department)
let intermediate = Course(name: "Growing Common Herbs", in: department)
let advanced = Course(name: "Caring for Tropical Plants", in: department)

intro.nextCourse = intermediate
intermediate.nextCourse = advanced
department.courses = [intro, intermediate, advanced]

上述代码创建了一个部门及其三个课程。入门课程和中级课程都存储了在它们的 nextCourse 属性中的建议下一门课程,该属性维护了一个对学生完成这门课程后应选修的课程的非所有者可选引用。

Unowned Optional References

未拥有的可选引用不会对它所包装的类的实例保持强引用,因此不会阻止 ARC 释放该实例。它在 ARC 下的行为与未拥有的引用相同,只是可选的未拥有的引用可以是 nil

与非可选的未拥有的引用类似,您需要确保 nextCourse 始终指向一个未被释放的课程。例如,在从 department.courses 中删除课程时,您还需要移除其他课程可能拥有的对该课程的任何引用。

注意

可选值的基础类型是 Optional ,这是 Swift 标准库中的一个枚举。然而,可选值是值类型不能标记为 unowned 规则的例外。

包装类的可选值不使用引用计数,因此您不需要保持对可选值的强引用。

未拥有引用和隐式未包装的可选属性

上面关于弱引用和未拥有引用的示例涵盖了需要打破强引用循环的两种更常见的情况。

PersonApartment 示例展示了两个都允许为 nil 的属性可能造成强引用循环的情况。这种场景最好使用弱引用来解决。

CustomerCreditCard 示例展示了其中一个允许为 nil 的属性和另一个不允许为 nil 的属性可能造成强引用循环的情况。这种场景最好使用未拥有引用来解决。

然而,还有一种情况,即两个属性在初始化完成后始终应该有值,且两个属性都不应该初始化完成后为 nil 。在这种情况下,可以在一个类中使用非拥有的属性,在另一个类中使用隐式解包的可选属性。

这样,在初始化完成后,可以直接访问这两个属性(无需解包),同时仍然避免了引用循环。本节将向您展示如何设置这种关系。

以下示例定义了两个类 CountryCity ,每个类都将其他类的一个实例作为属性存储。在这个数据模型中,每个国家必须始终有一个首都,每个城市也必须始终属于一个国家。为了表示这一点, Country 类有一个 capitalCity 属性,而 City 类有一个 country 属性:

swift
class Country {
    let name: String
    var capitalCity: City!
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

为了在两个类之间设置这种依赖关系, City 类的初始化器接受一个 Country 实例,并将其存储在其 country 属性中。

City 的初始化器在 Country 的初始化器内部被调用。然而, Country 的初始化器在 self 完全初始化之前不能将 self 传递给 City 的初始化器,如两阶段初始化中所述。

为了满足这一要求,你将 capitalCity 属性声明为隐式解包的可选属性,通过在其类型注解末尾添加感叹号( Country )来表示。这意味着 capitalCity 属性具有默认值 nil ,就像其他可选属性一样,但可以在不需要解包其值的情况下访问它,如隐式解包的可选属性中所述。

由于 capitalCity 具有默认 nil 值,因此只要 Country 实例在其初始化器中设置其 Country 属性,Country 实例就被认为已经完全初始化。这意味着 Country 的初始化器可以一但 name 属性被设置就可以开始引用和传递隐式的 self 属性。因此, Country 的初始化器可以在设置其自己的 capitalCity 属性时将 self 作为 City 初始化器的一个参数传递给 Country 初始化器。

这意味着你可以在一个语句中创建 CountryCity 实例,而不会创建强引用循环,并且可以直接访问 capitalCity 属性,而不需要使用感叹号来解包其可选值:

swift
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"

在上面的示例中,使用隐式解包的可选值意味着满足了两个阶段类初始化的所有要求。一旦初始化完成, capitalCity 属性可以像非可选值一样使用和访问,同时仍然避免了强引用循环。

强引用循环与闭包

你上面看到了当两个类实例属性彼此持有强引用时如何创建强引用循环。你还看到了如何使用弱引用和未拥有引用来打破这些强引用循环。

如果你将一个闭包赋值给类实例的一个属性,并且闭包的主体捕获了该实例,也可能发生强引用循环。这种捕获可能是因为闭包的主体访问了实例的属性,例如 self.someProperty ,或者是因为闭包调用了实例的方法,例如 self.someMethod() 。无论哪种情况,这些访问都会使闭包“捕获” self ,从而创建一个强引用循环。

这个强引用循环是因为闭包像类一样,是一种引用类型。当你将一个闭包赋值给一个属性时,你实际上是在赋值该闭包的引用。本质上,这与上述问题相同——两个强引用互相保持对方存活。不过,这次不是两个类实例,而是类实例和一个闭包互相保持对方存活。

Swift 为解决这个问题提供了一个优雅的解决方案,称为闭包捕获列表。然而,在学习如何使用闭包捕获列表来打破强引用循环之前,了解这种循环是如何产生的很有用。

以下示例展示了如何在使用引用 self 的闭包时创建强引用循环。该示例定义了一个名为 HTMLElement 的类,该类为 HTML 文档中的单个元素提供了一个简单的模型:

swift
class HTMLElement {
    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

HTMLElement 类定义了一个 name 属性,该属性表示元素的名称,例如对于标题元素使用 "h1" ,对于段落元素使用 "p" ,对于换行元素使用 "br"HTMLElement 类还定义了一个可选的 text 属性,您可以将其设置为表示要在该 HTML 元素内呈现的文本的字符串。

除了这两个简单的属性之外, HTMLElement 类还定义了一个名为 asHTML 的懒属性。该属性引用了一个闭包,该闭包将 nametext 结合成一个 HTML 字符串片段。 asHTML 属性的类型为 () -> String ,即“不带参数的函数,返回一个 String 值”。

默认情况下, asHTML 属性被分配了一个闭包,该闭包返回一个 HTML 标签的字符串表示。如果存在 text 值,则包含该值;如果不存在 text 值,则不包含任何文本内容。对于段落元素,闭包将返回 "<p>some text</p>""<p />" ,具体取决于 text 属性等于 "some text" 还是 nil

asHTML 属性被命名并像实例方法一样使用。然而,由于 asHTML 是一个闭包属性而不是实例方法,因此您可以将 asHTML 属性的默认值替换为自定义闭包,以便更改特定 HTML 元素的 HTML 渲染。

例如, asHTML 属性可以设置为一个默认返回某些文本的闭包,如果 text 属性是 nil ,以防止表示返回空的 HTML 标签。

swift
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Prints "<h1>some default text</h1>"

注意

asHTML 属性声明为一个懒加载属性,因为它只有在实际需要将元素渲染为字符串值以供某些 HTML 输出目标使用时才需要。由于 asHTML 是一个懒加载属性,因此您可以在默认闭包中引用 self ,因为懒加载属性将在初始化完成后才被访问,并且此时 self 已经存在。

HTMLElement 类提供了一个单一的初始化器,该初始化器接受一个 name 参数(如果需要的话),并初始化一个新的元素。该类还定义了一个析构函数,该析构函数打印一条消息以显示何时销毁一个 HTMLElement 实例。

这是如何使用 HTMLElement 类创建并打印一个新实例的方法:

swift
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

注意

上面的 paragraph 变量被定义为可选的 HTMLElement ,因此可以在下面将其设置为 nil 以演示强引用循环的存在。

不幸的是,上面编写的 HTMLElement 类在默认的 asHTML 值中使用闭包时,会在 HTMLElement 实例和其闭包之间创建一个强引用循环。循环如下所示:

Closure Reference Cycle

实例的 asHTML 属性持有对其闭包的强引用。然而,由于闭包在其主体中引用了 self (作为引用 self.nameself.text 的一种方式),闭包捕获了 self,这意味着它也持有对 HTMLElement 实例的强引用。因此在两者之间创建了一个强引用循环。(有关闭包中捕获值的更多信息,请参阅捕获值。)

注意

尽管闭包多次引用了 self ,但它只捕获了一个对 HTMLElement 实例的强引用。

如果你将 paragraph 变量设置为 nil 并断开其与 HTMLElement 实例的强引用,强引用循环会阻止释放 HTMLElement 实例及其闭包:

swift
paragraph = nil

注意 HTMLElement 去初始化器中的消息没有被打印,这表明 HTMLElement 实例没有被释放。

解决闭包中的强引用循环

通过在闭包定义中定义捕获列表来解决闭包与类实例之间的强引用循环。捕获列表定义了在闭包主体中捕获一个或多个引用类型时使用的规则。就像两个类实例之间的强引用循环一样,您需要将每个捕获的引用声明为弱引用或非所有者引用,而不是强引用。弱引用或非所有者引用的选择取决于代码不同部分之间的关系。

注意

每当您在闭包中引用 self 的成员时,Swift 要求您写 self.somePropertyself.someMethod() (而不是 somePropertysomeMethod() )。这有助于您记住有可能意外捕获 self

定义捕获列表

捕获列表中的每个项都是 weakunowned 关键字与其对某个类实例(如 self )或已初始化为某个值的变量(如 delegate = self.delegate )的引用的配对。这些配对写在一对方括号内,用逗号分隔。

在闭包的参数列表和返回类型(如果提供的话)之前放置捕获列表:

swift
lazy var someClosure = {
        [unowned self, weak delegate = self.delegate]
        (index: Int, stringToProcess: String) -> String in
    // closure body goes here
}

如果闭包没有指定参数列表或返回类型,因为可以从上下文中推断出来,则将捕获列表放在闭包的最前面,紧跟 in 关键字:

swift
lazy var someClosure = {
        [unowned self, weak delegate = self.delegate] in
    // closure body goes here
}

弱引用和未拥有引用

在闭包和闭包所捕获的实例总是互相引用并且总是同时被释放的情况下,将闭包中的捕获定义为非所有者引用。

相反,在捕获的引用在未来某个时刻可能变为 nil 的情况下,将捕获定义为弱引用。弱引用总是可选类型,并且在所引用的实例被释放时会自动变为 nil 。这可以在闭包体中检查它们的存在。

注意

如果捕获的引用永远不会变为 nil ,则应始终将其作为非所有者引用而不是弱引用进行捕获。

非所有者引用是用于解决上面强引用循环中闭包示例 HTMLElement 中的强引用循环的适当捕获方法。这是如何编写 HTMLElement 类以避免循环的示例:

swift
class HTMLElement {
    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
            [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

这个 HTMLElement 的实现与之前的实现相同,除了在 asHTML 闭包中添加了一个捕获列表。在这种情况下,捕获列表是 [unowned self] ,这意味着“捕获 self 为一个非所有者引用而不是一个强引用”。

你可以像之前那样创建并打印一个 HTMLElement 实例:

swift
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

这里是捕获列表生效后的引用情况:

Closure Reference Cycle Resolved

这次,闭包对 self 的捕获是一个非所有者引用,并不会对 HTMLElement 实例保持强引用。如果你将 paragraph 变量对 nil 的强引用设置为 HTMLElementHTMLElement 实例将被释放,如下例中的销毁消息所示:

swift
paragraph = nil
// Prints "p is being deinitialized"

对于捕获列表的更多信息,请参见捕获列表