Swift使用自动引用计数(ARC)来跟踪和管理应用程序的内存使用情况。在大多数情况下,这意味着内存管理在Swift中“有效”,您无需自己考虑内存管理。当不再需要类实例使用的实例时,ARC会自动释放它们。
但是,在某些情况下,ARC需要更多有关代码部分之间关系的信息,以便为您管理内存。本章介绍了这些情况,并说明了如何启用ARC来管理应用程序的所有内存。在Swift中使用ARC与在将ARC与Objective-C一起使用过渡到ARC发行说明中描述的方法非常相似。
引用计数仅适用于类的实例。结构和枚举是值类型,而不是引用类型,并且不通过引用存储和传递。
ARC如何运作
每次创建类的新实例时,ARC都会分配一块内存来存储有关该实例的信息。该内存保存有关实例类型的信息,以及与该实例关联的任何已存储属性的值。
此外,当不再需要某个实例时,ARC会释放该实例使用的内存,以便该内存可用于其他目的。这样可确保不再需要类实例时,它们不会占用内存空间。
但是,如果ARC要取消分配仍在使用的实例,则将无法再访问该实例的属性或调用该实例的方法。确实,如果您尝试访问该实例,则您的应用很可能会崩溃。
为了确保实例在仍然需要时不会消失,ARC跟踪当前引用每个类实例的属性,常量和变量的数量。只要仍存在至少一个对实例的有效引用,ARC便不会取消分配该实例。
为此,每当您将类实例分配给属性,常量或变量时,该属性,常量或变量都会强烈引用该实例。该引用被称为“强”引用,因为它可以使该实例保持牢靠,并且只要该强引用仍然存在,就不允许对其进行重新分配。
ARC在行动
这是一个有关自动引用计数工作方式的示例。本示例从名为的简单类开始,该类Person
定义了称为的存储常量属性name
:
- 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
类还具有当类的实例被释放,打印的消息的deinitializer。
下一个代码片段定义了三个type类型的变量Person?
,这些变量用于Person
在后续代码片段中设置对新实例的多个引用。由于这些变量是可选类型(Person?
,不是Person
),因此它们会自动以值初始化nil
,并且当前未引用Person
实例。
- var reference1: Person?
- var reference2: Person?
- var reference3: Person?
现在,您可以创建一个新Person
实例,并将其分配给以下三个变量之一:
- reference1 = Person(name: "John Appleseed")
- // Prints "John Appleseed is being initialized"
请注意,该消息将在调用类的初始化程序的位置打印。这确认初始化已发生。"John Appleseed is being initialized"
Person
由于Person
已将新实例分配给reference1
变量,因此现在有了reference1
对新Person
实例的强引用。因为至少有一个强有力的参考,所以ARC确保将其Person
保留在内存中并且不会被释放。
如果将同一Person
实例分配给另外两个变量,则将建立对该实例的两个更强引用:
- reference2 = reference1
- reference3 = reference1
现在,有三个强烈引用此单个Person
实例。
如果通过分配nil
两个变量来破坏其中两个强引用(包括原始引用),则将保留单个强引用,并且Person
不会释放该实例:
- reference1 = nil
- reference2 = nil
Person
直到第三个和最后一个强引用中断,ARC才会取消分配实例,这时很明显您不再使用该Person
实例:
- reference3 = nil
- // Prints "John Appleseed is being deinitialized"
类实例之间的强引用循环
在上面的示例中,ARC能够跟踪对Person
您创建的新实例的引用数量,并在Person
不再需要该实例时将其取消分配。
但是,可以编写这样的代码,其中类的实例永远不会达到强引用为零的程度。如果两个类实例相互之间有很强的引用,从而使每个实例使另一个实例保持活动状态,则可能发生这种情况。这被称为强参考周期。
通过将类之间的某些关系定义为弱引用或无主引用而不是强引用,可以解决强引用循环问题。解决类实例之间的强引用循环中介绍了此过程。但是,在学习如何解决强参考循环之前,了解这种循环是如何产生的很有用。
这是一个如何意外创建强参考循环的示例。此示例定义了两个名为Person
和的类Apartment
,它们对一组公寓及其居民进行建模:
- 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
type类型的属性String
和一个apartment
最初为的可选属性nil
。该apartment
属性是可选的,因为一个人可能并不总是拥有公寓。
同样,每个Apartment
实例都具有unit
类型的属性,String
并具有tenant
最初为的可选属性nil
。承租人属性是可选的,因为公寓可能并不总是有承租人。
这两个类都定义了一个反初始化器,它显示出该类的实例正在被反初始化的事实。这使您可以查看Person
和的实例是否Apartment
按预期进行了释放。
下一个代码片段定义了两个可选类型的变量,称为john
和unit4A
,这些变量将在下面设置为特定的Apartment
和Person
实例。这两个变量的初始值均为nil
,这是可选参数:
- var john: Person?
- var unit4A: Apartment?
现在,您可以创建一个特定的Person
实例和Apartment
实例,并将这些新实例分配给john
和unit4A
变量:
- john = Person(name: "John Appleseed")
- unit4A = Apartment(unit: "4A")
这是创建和分配这两个实例后强引用的外观。john
现在,变量对新Person
实例具有强引用,而unit4A
变量对新实例具有强引用Apartment
:
现在,您可以将两个实例链接在一起,以便此人拥有一个公寓,并且该公寓有一个租户。请注意,感叹号(!
)用于解包和访问存储在john
和unit4A
可选变量中的实例,以便可以设置这些实例的属性:
- john!.apartment = unit4A
- unit4A!.tenant = john
将两个实例链接在一起后,以下是强引用的外观:
不幸的是,链接这两个实例会在它们之间创建强大的参考周期。该Person
实例现在具有很强的参考Apartment
实例,该Apartment
实例具有很强的参考Person
实例。因此,当打破由john
和unit4A
变量持有的强引用时,引用计数不会降为零,并且实例也不会被ARC释放:
请注意,将这两个变量设置为时,都不会调用任何反初始化程序nil
。强大的参考周期可防止Person
和Apartment
实例被重新分配,从而导致应用程序中的内存泄漏。
将john
andunit4A
变量设置为时,以下是强引用的外观nil
:
Person
实例与Apartment
实例之间的强引用保持不变,并且不能被破坏。
解决类实例之间的强引用循环
当您使用类类型的属性时,Swift提供了两种解决强引用循环的方法:弱引用和无主引用。
弱引用和不带所有权的引用使引用周期中的一个实例可以引用另一个实例,而无需对其进行严格控制。然后,这些实例可以相互引用,而无需创建强大的引用周期。
当另一个实例的生存期较短时(即,另一个实例可以首先被释放时),请使用弱引用。在Apartment
上面的示例中,对于公寓来说,在其生命周期中的某个时候能够没有租户是合适的,因此在这种情况下,弱引用是打破引用周期的一种适当方法。相反,当另一个实例具有相同的生存期或更长的生存期时,请使用无主引用。
参考文献薄弱
一个弱引用是不保留对实例的强抱它指的是,所以不从引用的实例处置停止ARC的参考。此行为可防止参考成为强大参考周期的一部分。您可以通过将weak
关键字放在属性或变量声明之前来指示弱引用。
由于弱引用不能完全控制它所引用的实例,因此有可能在弱引用仍在引用该实例时将其释放。因此,ARC将自动设置一个弱引用,以nil
使其所引用的实例被重新分配。而且,由于弱引用需要允许nil
在运行时将其值更改为,因此它们始终被声明为可选类型的变量,而不是常量。
您可以像其他任何可选值一样检查弱引用中是否存在值,并且永远不会以对不再存在的无效实例的引用结束。
笔记
当ARC对nil
。设置弱引用时,不调用属性观察器。
下面的示例与上面的Person
和Apartment
示例相同,但有一个重要区别。这次,Apartment
类型的tenant
属性被声明为弱引用:
- 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") }
- }
像以前一样,创建了两个变量(john
和unit4A
)的强引用以及两个实例之间的链接:
- var john: Person?
- var unit4A: Apartment?
-
- john = Person(name: "John Appleseed")
- unit4A = Apartment(unit: "4A")
-
- john!.apartment = unit4A
- unit4A!.tenant = john
将两个实例链接在一起后,这些引用的外观如下:
Person
实例仍然具有对该实例的强引用Apartment
,但是该Apartment
实例现在具有对该实例的弱引用Person
。这意味着,当您通过将john
变量设置为来破坏变量所nil
拥有的强引用时,将不再有对该Person
实例的强引用:
- john = nil
- // Prints "John Appleseed is being deinitialized"
由于不再有对该Person
实例的更强引用,因此将其释放,并将该tenant
属性设置为nil
:
对Apartment
实例的唯一唯一强引用来自unit4A
变量。如果您破坏了该强引用,则不再有对该Apartment
实例的强引用:
- unit4A = nil
- // Prints "Apartment 4A is being deinitialized"
由于不再有对该Apartment
实例的更强引用,因此也将其释放:
笔记
在使用垃圾回收的系统中,弱指针有时用于实现简单的缓存机制,因为没有强引用的对象仅在内存压力触发垃圾回收时才被释放。但是,使用ARC时,值将在删除最后一个强引用后立即释放,这使弱引用不适用于此目的。
无人参考
像弱引用一样,无主引用也不会对其引用的实例保持强大的控制力。但是,与弱引用不同,当另一个实例具有相同的生存期或更长的生存期时,将使用无主引用。您可以通过将unowned
关键字放在属性或变量声明之前来指示无主引用。
与弱引用不同,期望无所有权引用始终具有值。因此,将值标记为未拥有不会使其成为可选,并且ARC永远不会将未拥有的引用的值设置为nil
。
重要的
仅当确定引用始终引用尚未取消分配的实例时,才使用无主引用。
如果在释放该实例后尝试访问一个未拥有的引用的值,则会收到运行时错误。
下面的示例定义两个类Customer
和CreditCard
,它们分别为银行客户和该客户的可能的信用卡建模。这两个类各自将另一个类的实例存储为属性。这种关系有可能建立一个强大的参考周期。
之间的关系Customer
和CreditCard
距离之间的关系略有不同Apartment
,并Person
在上面的弱参考例所示。在此数据模型中,客户可能拥有或可能没有信用卡,但是信用卡将始终与该客户相关联。一个CreditCard
实例永远会超越的Customer
,它指的是。为了表示这一点,Customer
该类具有可选card
属性,但是CreditCard
该类具有未拥有(且非可选)customer
属性。
此外,只能通过将值和实例传递给自定义初始化程序来创建新CreditCard
实例。这样可以确保在创建实例时,实例始终具有与之关联的实例。number
customer
CreditCard
CreditCard
customer
CreditCard
由于信用卡将始终有客户,因此您可以将其customer
属性定义为无主引用,以避免出现强大的引用周期:
- 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
属性的容量足以在32位和64位系统上存储16位卡号。
下一个代码片段定义了一个Customer
名为的可选变量john
,该变量将用于存储对特定客户的引用。由于该变量是可选的,因此其初始值为nil:
现在Customer
,您可以创建一个实例,并使用它来初始化新CreditCard
实例并将其分配为该客户的card
属性:
- john = Customer(name: "John Appleseed")
- john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
既然您已经链接了两个实例,则引用的外观如下:
该Customer
实例现在具有很强的参考CreditCard
实例,该CreditCard
实例有一个无主的参考Customer
实例。
由于无主customer
引用,因此当您破坏john
变量持有的强引用时,将不再有对该Customer
实例的强引用:
因为不再有对该Customer
实例的强引用,所以将其释放。在这种情况发生之后,将不再有对该CreditCard
实例的更强引用,并且也将其释放:
- john = nil
- // Prints "John Appleseed is being deinitialized"
- // Prints "Card #1234567890123456 is being deinitialized"
上面的最后一个代码片段显示,Customer
实例和CreditCard
实例的反初始化器在将john
变量设置为之后都打印其“反初始化”消息nil
。
笔记
上面的示例说明了如何使用安全的未拥有引用。在需要禁用运行时安全检查的情况下,例如出于性能原因,Swift还提供了不安全的无主引用。与所有不安全的操作一样,您有责任检查该代码的安全性。
您通过写来表示不安全的,未拥有的引用unowned(unsafe)
。如果在释放引用的实例后尝试访问不安全的无主引用,则程序将尝试访问该实例以前所在的内存位置,这是不安全的操作。
无主的可选参考
您可以将对类的可选引用标记为未拥有。就ARC所有权模型而言,可以在相同的上下文中使用未拥有的可选引用和弱引用。区别在于,当您使用无主的可选引用时,您有责任确保其始终引用有效对象或将其设置为nil
。
这是一个跟踪学校特定部门开设的课程的示例:
- 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
属性是可选的。
这是使用这些类的示例:
- 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
属性中存储了建议的下一门课程,该课程为学生完成此课程后应选择的课程提供了无用的可选参考。
一个无主的可选引用不会对其包装的类的实例保持强大的控制,因此也不会阻止ARC取消分配该实例。它的行为与ARC下无主引用相同,只是无主可选引用可以为nil
。
像非可选的无主引用一样,您有责任确保nextCourse
始终引用尚未取消分配的课程。例如,在这种情况下,当您从中删除课程时,department.courses
您还需要删除其他课程可能具有的对该课程的任何引用。
笔记
可选值的基础类型是Optional
,这是Swift标准库中的一个枚举。但是,可选选项是不能用标记值类型的规则的例外unowned
。
包装类的可选控件不使用引用计数,因此您无需维护对可选控件的强引用。
无主引用和隐式展开的可选属性
上面关于弱引用和无主引用的示例涵盖了两个较常见的情况,其中有必要打破一个强引用周期。
在Person
和Apartment
实施例显示的情况下两个属性,这两者都允许是nil
,有可能造成很强的参考周期。最好使用弱引用来解决此情况。
在Customer
与CreditCard
示例显示的情况下这是允许的一个属性是nil
和另一个属性,不能nil
有引起强烈的参考周期的潜力。最好使用无主引用来解决此情况。
但是,存在第三种情况,在这两种情况下,两个属性都应始终具有值,并且nil
初始化完成后,这两个属性都不应具有。在这种情况下,将一个类的未拥有属性与另一类的隐式展开的可选属性组合起来很有用。
这样,初始化完成后就可以直接访问这两个属性(没有可选的展开),同时仍然避免了引用周期。本节说明如何建立这种关系。
下面的示例定义两个类Country
和City
,每个类将另一个类的实例存储为属性。在此数据模型中,每个国家必须始终拥有一个首都,并且每个城市都必须始终属于一个国家。为了表示这一点,Country
该类具有一个capitalCity
属性,而City
该类具有一个country
属性:
- 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
给初始值设定项。City
Country
为了满足此要求,您可以将的capitalCity
属性声明Country
为隐式未包装的可选属性,由其类型注释(City!
)末尾的感叹号指示。这意味着该capitalCity
属性具有默认值nil
,就像其他任何可选属性一样,但是可以访问该属性,而无需如Implicitly Unwrapped Optionals中所述解开其值。
因为capitalCity
具有默认nil
值,所以Country
一旦Country
实例name
在其初始值设定项中设置了其属性,便认为该新实例已完全初始化。这意味着Country
初始化器可以self
在name
属性设置后立即开始引用并传递隐式属性。Country
因此self
,City
当Country
初始化程序设置自己的capitalCity
属性时,可以将初始化程序作为初始化程序的参数之一传递。
所有这些意味着您可以在单个语句中创建Country
andCity
实例,而无需创建一个强大的引用周期,并且capitalCity
可以直接访问该属性,而无需使用感叹号来包装其可选值:
- 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文档中的单个元素提供了一个简单的模型:
- 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
。此属性引用一个合并name
并text
合并为HTML字符串片段的闭包。该asHTML
属性的类型为,或“一个不带参数并返回值的函数”。() -> String
String
默认情况下,为asHTML
属性分配一个闭包,该闭包返回HTML标记的字符串表示形式。text
如果存在,则此标签包含可选值;如果不存在,则不包含文本内容text
。对于段落元素,闭包将返回或,具体取决于属性是否等于或。"<p>some text</p>"
"<p />"
text
"some text"
nil
该asHTML
属性的命名和使用有点像实例方法。但是,由于
请发表评论