自动引用计数
Swift使用自动引用计数(ARC)来跟踪和管理应用程序的内存使用情况。在大多数情况下,这意味着内存管理在Swift中“正常工作”,并且您不需要自己考虑内存管理。当这些实例不再需要时,ARC会自动释放类实例使用的内存。
但是,在少数情况下,ARC需要有关代码各部分之间关系的更多信息才能为您管理内存。本章将介绍这些情况,并说明如何使ARC能够管理所有应用程序的内存。在Swift中使用ARC非常类似于在Objective-C中使用ARC的过渡到ARC版本注释中描述的方法。
引用计数仅适用于类的实例。结构和枚举是值类型,而不是引用类型,不会通过引用存储和传递。
ARC如何工作
每次创建类的新实例时,ARC都会分配一块内存来存储有关该实例的信息。该内存保存关于实例类型的信息以及与该实例关联的任何存储属性的值。
此外,当不再需要实例时,ARC将释放该实例使用的内存,以便内存可用于其他目的。这可以确保类实例在不再需要时不占用内存空间。
但是,如果ARC要释放一个仍在使用的实例,将不再可能访问该实例的属性,或者调用该实例的方法。事实上,如果你试图访问实例,你的应用很可能会崩溃。
为了确保实例在仍然需要时不会消失,ARC会跟踪当前引用每个类实例的属性,常量和变量的数量。只要至少有一个对该实例的活动引用仍然存在,ARC就不会解除分配实例。
为了做到这一点,无论何时将一个类实例分配给一个属性,常量或变量,该属性,常量或变量都会强制引用该实例。该参考文献被称为 “strong” 参考文献,因为它在该实例中保持坚定的态度,并且只要 “strong” 的参考文献保留就不允许它被重新分配。
ARC in Action
以下是自动引用计数如何工作的示例。这个例子从一个叫Person的简单类开始,它定义了一个名为name的存储常量属性:
class Person {
let name: String
init(name: String) {
= name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
Person类有一个初始化程序,它设置实例的name属性并打印一条消息以指示正在进行初始化。 Person类还有一个deinitializer,当类的一个实例被释放时打印一条消息。
下一个代码片段定义了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正在初始化”打印在您调用Person类的初始化程序的位置。 这证实初始化已经发生。
由于新Person实例已分配给reference1变量,因此现在有一个从reference1到新Person实例的强引用。 因为至少有一个强引用,ARC确保这个Person保存在内存中并且不被释放。
如果您将同一个Person实例分配给两个更多的变量,则会建立对该实例的两个更强的引用:
reference2 = reference1
reference3 = reference1
现在有三个强大的对这个单个Person实例的引用。
如果通过将nil分配给两个变量来破坏这些强引用(包括原始引用)中的两个,则单个强引用仍然存在,并且不会释放Person实例:
reference1 = nil
reference2 = nil
ARC不会释放Person实例,直到第三个也是最后一个强引用被破坏,此时很明显您不再使用Person实例:
reference3 = nil
// Prints "John Appleseed is being deinitialized"
类实例之间的强引用循环
在上面的示例中,ARC能够跟踪您创建的新Person实例的引用数量,并在不再需要该实例时释放该实例。
但是,可以编写一个代码,其中一个类的实例永远不会到达其强引用为零的点。如果两个类实例之间存在强关联,那么每个实例都会保持另一个实例的活动。这被称为强引用循环。
通过将类之间的某些关系定义为弱引用或无主引用而不是强引用,可以解决强引用循环。在解决类实例之间的强引用循环中描述了此过程。但是,在您学习如何解决强引用循环之前,了解如何导致这样的周期很有用。
下面是一个例子,说明如何强制创建一个参考周期。这个例子定义了两个叫Person和Apartment的类,它模拟了一组公寓和其居民的模型:
class Person {
let name: String
init(name: String) { = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
每个Person实例都有一个String类型的名称属性和一个最初为零的Apartment?属性。 Apartment?属性是可选的,因为一个人可能并不总是有公寓。
同样,每个Apartment实例都有一个String类型的单元属性,并且有一个可选的租户属性,最初为零。 租户属性是可选的,因为公寓可能并不总是有租客。
这两个类都定义了一个deinitializer,它打印出该类的一个实例正在被初始化的事实。 这使您可以查看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取消分配:
john = nil
unit4A = nil
请注意,当您将这两个变量设置为零时,不会调用deinitializer。 强大的引用周期可防止Person和Apartment实例被解除分配,从而导致应用程序内存泄漏。
以下是将您的john和unit4A变量设置为零后,强参考的效果如何:
Person实例和Apartment实例之间的强引用依然存在并且不能被破坏。
解决类实例之间的强引用循环
当您使用类类型的属性时,Swift提供了两种解决强引用周期的方法:弱引用和无主引用( weak references and unowned references.)。
弱引用和无主引用使参考周期中的一个实例能够引用另一个实例,而不会对其保持强有力的保留。然后这些实例可以互相引用,而不会创建强大的参考周期。
当另一个实例的生命周期较短时,使用弱引用 - 也就是说,当其他实例可以先释放时。在上面的公寓示例中,公寓在其生命周期的某个时间点能够没有租户是合适的,因此弱参考是在此情况下打破参考周期的适当方式。相反,当另一个实例具有相同的生命周期或更长的生命周期时,请使用无主引用。
弱引用
一个弱引用是一个引用,它不会强制保留它引用的实例,所以不会停止ARC处理引用的实例。此行为可防止参考成为强参考周期的一部分。通过在属性或变量声明之前放置weak关键字来指示弱引用。
因为弱引用不会强引用它所引用的实例,所以可以将该实例解除分配,而弱引用仍然指向它。因此,当它引用的实例被释放时,ARC自动将弱引用设置为零。而且,因为弱引用需要允许它们的值在运行时更改为零,所以它们总是被声明为可选类型的变量而不是常量。
您可以检查弱引用中是否存在值,就像其他任何可选值一样,并且永远不会引用对不再存在的无效实例的引用。
注意
当ARC将弱引用设置为零时,不会调用属性观察器。
以下示例与上面的Person和Apartment示例相同,但有一个重要区别。这一次,Apartment类型的Apartment属性被声明为弱引用:
class Person {
let name: String
init(name: String) { = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { = 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实例有一个弱引用。 这意味着当你通过设置为nil来打破john变量的强引用时,没有更强的对Person实例的引用:
john = nil
// Prints "John Appleseed is being deinitialized"
由于没有更强的对Person实例的引用,因此将其解除分配,并将租户属性设置为零:
剩余的对Apartment实例的强大引用来自unit4A变量。 如果你打破了这个强大的引用,那么就没有更强的对Apartment实例的引用:
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
由于没有更强的对Apartment实例的引用,它也被解除分配:
注意
在使用垃圾收集的系统中,弱指针有时用于实现简单的缓存机制,因为只有当内存压力触发垃圾收集时才会释放没有强引用的对象。但是,使用ARC时,只要删除最后一个强参考值就会释放值,从而导致弱参考不适用于此目的。
无主的参考
就像一个弱引用一样,一个无主的引用并不能很好地保持它引用的实例。然而,与弱引用不同,当另一个实例具有相同的生命周期或更长的生命周期时,将使用无主引用。您通过在属性或变量声明之前放置无主关键字来指示无主引用。
预计一个无主的参考将始终有一个值。因此,ARC从不将无主引用的值设置为零,这意味着无主引用是使用非选项类型定义的。
重要
只有当您确定引用始终引用未被释放的实例时才使用无主引用。
如果尝试在该实例解除分配后访问无主引用的值,则会出现运行时错误。
以下示例定义了两个类:Customer和CreditCard,它们为银行客户建立模型并为该客户建立可能的信用卡。这两个类都将另一个类的实例存储为一个属性。这种关系有可能创造一个强大的参考周期。
客户和CreditCard之间的关系与上面弱参考示例中看到的Apartment和Person之间的关系略有不同。在这种数据模型中,客户可能有也可能没有信用卡,但信用卡将始终与客户相关联。 CreditCard实例永远不会超过它引用的客户。为了表示这一点,客户类有一个可选的卡属性,但CreditCard类拥有一个无主的(和非可选的)客户属性。
此外,新的CreditCard实例只能通过将数字值和客户实例传递给自定义CreditCard初始值设定器来创建。这可确保CreditCard实例在创建CreditCard实例时始终拥有与其关联的客户实例。
由于信用卡将始终有客户,因此您将其客户资产定义为无主参考,以避免强烈的参考周期:
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
= name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned letcustomer: Customer
init(number: UInt64, customer: Customer) {
= number
= customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
注意
CreditCard类的number属性使用UInt64类型而不是Int定义,以确保number属性的容量足够大,可以在32位和64位系统上存储16位数的卡号。
下一个代码片段定义了一个名为john的可选Customer变量,该变量将用于存储对特定客户的引用。 由于可选,此变量的初始值为nil:
var john: Customer?
您现在可以创建一个Customer实例,并使用它初始化并分配一个新的CreditCard实例作为该客户的卡属性:
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
以下是引用的外观,现在已经链接了两个实例:
Customer实例现在具有对CreditCard实例的强引用,并且CreditCard实例拥有对Customer实例的非拥有引用。
由于无主客户参考,当您打破由john变量持有的强引用时,没有更强的对客户实例的引用:
由于没有更强的对客户实例的引用,因此将其解除分配。 发生这种情况后,没有更多的对CreditCard实例的强引用,并且它也被解除分配:
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
上面的最后一段代码显示了Customer实例和CreditCard实例的去初始化器在john变量设置为nil后,都会打印它们的“deinitialized”消息。
注意
上面的例子展示了如何使用安全的无主引用。 Swift还为需要禁用运行时安全检查的情况提供不安全的无主引用 - 例如,出于性能原因。与所有不安全的操作一样,您需要负责检查该代码的安全性。
您通过书写无主(不安全)表示不安全的无参考参考。如果尝试在其引用的实例解除分配后访问不安全的无主引用,程序将尝试访问实例曾经存在的内存位置,这是一种不安全的操作。
无主的引用和隐式解包的可选属性
上述弱和无主参考的例子涵盖了两个更常见的场景,其中有必要打破强大的参考周期。
Person和Apartment示例显示了两种属性都允许为零的属性有可能导致强烈的参考周期的情况。这种情况最好用弱引用解决。
Customer和CreditCard示例显示了一种情况,其中一个属性允许为零,另一个属性不能为零有可能导致强烈的参考周期。这种情况最好用无主的参考来解决。
class country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
= name
= City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned letcountry: Country
init(name: String, country: Country) {
= name
= country
}
}
为了建立这两个类之间的相互依赖关系,City的初始化程序需要一个Country实例,并将此实例存储在其country属性中。
City的初始化程序在Country的初始化程序中调用。但是,Country的初始化程序无法将自身传递给City初始化程序,直到新的Country实例完全初始化为止,如“两阶段初始化”中所述。
为了应付这个要求,你将国家的capitalCity属性声明为一个隐式解包的可选属性,由其类型注释(City!)末尾的感叹号表示。这意味着capitalCity属性的默认值为nil,就像任何其他可选属性一样,但可以在隐式展开选项中所述的情况下访问而无需解开其值。
因为capitalCity有一个默认的nil值,所以只要Country实例在其初始化程序中设置其名称属性,就会认为新的Country实例已完全初始化。这意味着只要设置了name属性,国家初始值设定项就可以开始引用并传递隐式自我属性。当Country初始化程序设置自己的capitalCity属性时,Country初始值设定项可以将自身作为City初始化程序的参数之一。
所有这些意味着您可以在单个语句中创建Country和City实例,而无需创建强大的引用循环,并且可以直接访问capitalCity属性,而无需使用感叹号来打开其可选值:
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\)'s capital city is called \)")
// Prints "Canada's capital city is called Ottawa"
在上面的示例中,使用隐式解包可选意味着满足所有两阶段类初始化器要求。一旦初始化完成,capitalCity属性可以被使用和访问为非可选值,同时仍然避免强烈的参考周期。
关闭的强参考周期
您在上面看到,当两个类实例属性彼此强关联时,如何创建强引用循环。您还看到了如何使用弱和无主的引用来打破这些强参考周期。
如果将闭包分配给类实例的属性,并且该闭包的主体捕获实例,则也会发生强引用循环。由于闭包的主体访问实例属性(如),或者因为闭包调用实例上的方法(如()),因此可能会发生此捕获。在任何一种情况下,这些访问都会导致关闭“捕捉”自我,从而形成强大的参照周期。
这种强大的参考循环发生是因为闭包,就像类一样,是引用类型。当您将一个闭包分配给一个属性时,您正在为该闭包分配一个引用。实质上,这与上面的问题是一样的 - 两个强有力的引用保持着彼此的活力。但是,这次不是两个类实例,而是一个保持对方活着的类实例和闭包。
Swift为这个问题提供了一个优雅的解决方案,称为闭包捕获列表。然而,在你学习如何用闭包捕获列表打破一个强大的引用循环之前,了解如何引起这样一个循环是很有用的。
下面的例子展示了如何使用引用self的闭包来创建强引用循环。这个例子定义了一个名为HTMLElement的类,它为HTML文档中的单个元素提供了一个简单模型:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
if let text = {
return"<\()>\(text)</\()>"
} else {
return"<\() />"
}
}
init(name: String, text: String? = nil) {
= name
= text
}
deinit {
print("\(name) is being deinitialized")
}
}
HTMLElement类定义了一个名称属性,它指示元素的名称,例如标题元素的“h1”,段落元素的“p”或换行符元素的“br”。 HTMLElement还定义了一个可选的文本属性,您可以将其设置为表示要在该HTML元素中呈现的文本的字符串。
除了这两个简单属性之外,HTMLElement类还定义了一个名为asHTML的惰性属性。该属性引用一个将名称和文本组合成一个HTML字符串片段的闭包。 asHTML属性的类型为() - > String,或者“不带参数的函数,并返回String值”。
默认情况下,为asHTML属性分配一个闭包,该闭包返回HTML标记的字符串表示形式。该标签包含可选的文本值(如果存在),或者如果文本不存在则不包含文本内容。对于段落元素,闭包将返回“<p>一些文本</ p>”或“<p />”,具体取决于text属性是否等于“某些文本”或nil。
asHTML属性的命名和使用有点像实例方法。但是,因为asHTML是闭包属性而非实例方法,所以如果要更改特定HTML元素的HTML呈现,可以使用自定义闭包来替换asHTML属性的默认值。
例如,为了防止表示返回一个空的HTML标记,可以将asHTML属性设置为一个闭包,该闭包在text属性为nil时默认为某些文本:
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
= {
return"<\)>\ ?? defaultText)</\)>"
}
print(())
// Prints "<h1>some default text</h1>"
注意
asHTML属性被声明为lazy属性,因为只有当元素实际需要呈现为某个HTML输出目标的字符串值时才需要它。 asHTML是一个懒惰属性的事实意味着你可以在默认闭包中引用self,因为只有在初始化完成并且已知self存在之后才会访问lazy属性。
HTMLElement类提供了一个初始化器,它接受一个名称参数和(如果需要的话)一个文本参数来初始化一个新元素。 该类还定义了一个deinitializer,它将打印一条消息以显示HTMLElement实例何时解除分配。
以下是使用HTMLElement类创建和打印新实例的方法:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
注意
上面的段落变量被定义为一个可选的HTMLElement,因此它可以设置为零以下,以证明存在一个强参考周期。
不幸的是,如上所述,HTMLElement类在HTMLElement实例和用于其默认asHTML值的闭包之间创建了强大的引用循环。 这是周期的外观:
该实例的asHTML属性对其关闭有很强的参考。 然而,因为闭包在自己的身体内引用了self(作为引用和的一种方式),闭包捕获self,这意味着它拥有一个强引用返回到HTMLElement实例。 两者之间建立了强大的参考循环。 (有关在闭包中捕获值的更多信息,请参阅捕获值。)
注意
即使闭包多次引用自身,它也只捕获对HTMLElement实例的强引用。
如果将段落变量设置为nil并将其对HTMLElement实例的强引用断开,则由于强引用循环,HTMLElement实例及其闭包都不会被释放:
paragraph = nil
请注意,HTMLElement deinitializer中的消息未打印,这表明HTMLElement实例未取消分配。
解决闭环强参考周期
通过将捕获列表定义为闭包定义的一部分,可以解决闭包与类实例之间的强引用循环。捕获列表定义了在封闭体内捕获一个或多个引用类型时使用的规则。与两个类实例之间的强引用周期一样,您将每个捕获的引用声明为弱引用或无主引用,而不是强引用。弱或无主的适当选择取决于代码不同部分之间的关系。
注意
无论何时在闭包中引用self的成员时,Swift都要求您编写或()(而不仅仅是someProperty或someMethod())。这可以帮助你记住,有可能意外发现自己。
定义一个捕获列表
捕获列表中的每个项目都是弱关键字或无主关键字与对类实例(如self)的引用或使用某个值初始化的变量(如delegate = !)的配对。这些配对写在一对方括号内,用逗号分隔。
如果提供它们,则将捕获列表放置在闭包的参数列表和返回类型之前:
lazy var someClosure: (Int, String) -> String = {
[unownedself, weakdelegate = !] (index: Int, stringToProcess: String) -> Stringin
// closure body goes here
}
如果闭包没有指定参数列表或返回类型,因为它们可以从上下文中推断出来,请将捕获列表放置在闭包的开头,然后是in关键字:
lazy var someClosure: () -> String = {
[unownedself, weakdelegate = !] in
// closure body goes here
}
弱和无主的参考
当闭包和它捕获的实例将始终相互引用时,将闭包中的捕获定义为无主引用,并且将始终在同一时间解除分配。
相反,当捕获的参考在未来某个时间点可能变为零时,将捕获定义为弱参考。 弱引用始终是可选类型,并且在它们引用的实例被释放时自动变为零。 这使您可以检查封闭体内的存在。
注意
如果捕获的引用永远不会变为零,则应始终将其捕获为无主引用,而不是弱引用。
一个无主引用是用于解决早先HTMLElement示例中的强引用周期的适当捕获方法。 下面介绍如何编写HTMLElement类以避免循环:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
[unownedself] in
if let text = {
return"<\()>\(text)</\()>"
} else {
return"<\() />"
}
}
init(name: String, text: String? = nil) {
= name
= text
}
deinit {
print("\(name) is being deinitialized")
}
}
除了在asHTML闭包中添加捕获列表之外,HTMLElement的这个实现与之前的实现完全相同。 在这种情况下,捕获列表是[无主自我],意思是“将自我捕获为无主引用而不是强引用”。
您可以像之前一样创建并打印一个HTMLElement实例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
以下是引用在捕获列表中的使用方式:
这一次,闭包捕获自己是一个无主的引用,并且不会很好地保留它捕获的HTMLElement实例。 如果您将段落变量的强引用设置为nil,则HTMLElement实例将被解除分配,这可以从下面示例中的deinitializer消息打印中看到:
paragraph = nil
// Prints "p is being deinitialized"
有关捕获列表的更多信息,请参阅捕获列表。