Swift编程二十五(自动引用计数)
案例代码下载
自动参考计数
Swift使用自动引用计数(ARC)来跟踪和管理应用程序的内存使用情况。在大多数情况下,这意味着内存管理在Swift中是“正常工作”,不需要考虑内存管理。当不再需要这些实例时,ARC会自动释放类实例使用的内存。
但是,在少数情况下,ARC需要有关代码部分之间关系的更多信息,以便为管理内存。本章介绍了这些情况,并说明了如何启用ARC来管理所有应用程序的内存。在Swift中使用ARC非常类似于bjective-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类有一个initializer设置实例的name属性并显示一条消息,指示初始化正在进行中。Person类还具有deinitializer,当类的实例销毁时打印的消息。
下一个代码片段定义了三个Person?类型的变量,用于在后续代码片段中设置对Person新实例的多个引用。由于这些变量属于可选类型(Person?而不是Person),因此它们会自动使用值nil初始化,并且当前不会引用Person实例。
var reference1: Person?
var reference2: Person?
var reference3: Person?
现在可以创建一个新Person实例并将其分配给以下三个变量之一:
reference1 = Person(name: "John Appleseed")
/*
打印结果:
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
/*
打印结果:
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实例都有一个String类型的name属性和一个初始化为nil的apartment可选属性。apartment属性是可选的,因为一个人可能并不总是有公寓。
类似地,每个Apartment实例都具有String类型的unit属性,并且具有初始化为nil的tenant可选属性。tenant属性是可选的,因为公寓可能并不总是有租户。
这两个类还定义了一个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实例的强引用:
image
现在可以将两个实例链接在一起,以便此人拥有公寓,并且公寓有租户。注意,感叹号(!)用于解包和访问存储在john和unit4A的可选变量,以便这些实例的属性可设置:
john!.apartment = unit4A
unit4A!.tenant = john
下是将两个实例链接在一起后强引用的外观:
image
不幸的是,链接这两个实例会在它们之间产生强引用循环。Person实例现在具有对该Apartment实例的强引用,并且该Apartment实例具有对该Person实例的强引用。因此,当断开john和unit4A变量所持有的强引用时,引用计数不会降为零,并且ARC不会释放实例:
john = nil
unit4A = nil
请注意,将这两个变量设置为nil时,都不会调用deinitializer 。强引用循环可防止Person和Apartment实例被销毁,从而导致应用程序内存泄漏。
以下是在将john和unit4A变量设置为nil以下后强引用的外观:
image
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
现在,将这两个实例链接在一起的引用循环如下:
image
Person实例仍然具有对Apartment实例的强引用,但Apartment实例现在具有对Person实例的弱引用。这意味着当通过将john变量设置为nil断开变量所持有的强引用时,不再有对该Person实例的强引用:
john = nil
/*
打印结果:
John Appleseed is being deinitialized
*/
因为没有对Person实例的强引用,所以它被释放并且tenant属性设置为nil:
image
Apartment实例唯一剩下的强引用来自unit4A变量。如果你破坏了那个强引用,那么对Apartment实例没有强引用:
unit4A = nil
/*
打印结果:
Apartment 4A is being deinitialized
*/
因为没有对Apartment实例的强引用,所以它也被释放:
image
注意: 在使用垃圾收集的系统中,弱指针有时用于实现简单的缓存机制,因为只有当内存压力触发垃圾收集时才会释放没有强引用的对象。但是,使用ARC时,一旦删除了最后一个强引用,就会释放值,使得弱引用失效。
无主引用
就像一个弱引用一样,无主引用不能对实例强行持有。但是,与弱引用不同,当一个实例具有相同的生命周期或更长的生命周期时,将使用无主引用。通过在属性或变量声明之前放置unowned关键字来指示无主引用。
一个无主参考应该总是有一个值。因此,ARC永远不会将无主引用的值设置为nil,这意味着使用非可选类型定义无主引用。
重要:
仅当确定引用始终引用尚未释放的实例时,才使用无主引用。
如果在销毁实例后尝试访问无主引用的值,则会出现运行时错误。
下面的示例定义两个类,Customer并且CreditCard,该模型表示银行客户和客户可能使用的信用卡。这两个类每个都将另一个类的实例存储为属性。这种关系有可能创建一个强引用循环。
Customer和CreditCard之间的关系跟上面的弱参考例所示的Person和Apartment之间的关系略有不同。在此数据模型中,客户可能拥有或不拥有信用卡,但信用卡将始终与客户相关联。一个CreditCard实例永远依赖Customer实例。为了表示这一点,Customer类具有可选的card属性,但CreditCard该类具有无主引用(和非可选)的customer属性。
此外,只通过将卡号传和客户递给CreditCard自定义initializer来创建新CreditCard实例。这可确保在创建CreditCard实例时始终具有与之关联的客户实例。
由于信用卡将始终拥有客户,因此将其customer属性定义为无主引用,以避免强引用循环:
class Customer {
let name: String
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") }
}
注意: CreditCard类的number属性定义为一种UInt64类型而不是Int,以确保该number属性的容量足以在32位和64位系统上存储16位卡号。
下一个代码片段定义了一个Customer名为的可选变量john,该变量将用于存储对特定客户的引用。由于是可选的,该变量的初始值为nil:
var john: Customer?
现在可以创建一个Customer实例,并使用它为客户的card属性赋值和初始化新CreditCard实例:
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
现在您已经链接了两个实例,这里是引用的外观:
image
Customer实例现在具有对CreditCard实例的强引用,并且CreditCard实例具有对Customer实例的无主引用。
由于客户是无主引用,当您破坏john变量持有的强引用时,不再有对该Customer实例的强引用:
image
因为没有对Customer实例的强引用,所以它被释放。发生这种情况后,CreditCard实例没有强引用,它也就被销毁:
john = nil
/*
打印结果:
John Appleseed is being deinitialized
Card #1234567890123456 is being deinitialized
*/
上面的最后一个代码片段显示Customer实例和CreditCard实例的deinitializers在john变量设置为nil后都打印了它们的“deinitialized”消息。
注意
上面的示例显示了如何使用安全的无主引用。Swift还提供不安全的无主参考 - 例如,出于性能原因需要禁用运行时安全检查的情况。与所有不安全的操作一样,需要负责检查该代码的安全性。
通过书面unowned(unsafe)表示不安全的无主参考。如果在取消分配引用的实例后尝试访问不安全的无主引用,则程序将尝试访问实例所在的内存位置,这是一种不安全的操作。
无主引用和隐式解包可选属性
上面的弱引用和无主引用的例子涵盖了两个更常见的场景,其中有必要打破一个强引用循环。
在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属性中。
在Country的initializer中调用City的initializer。但是,Country的initializer无法传递self给City的initializer,直到新Country实例完全初始化为止,如两阶段初始化中所述。
要处理此要求,将capitalCity属性声明Country为隐式解包的可选属性,其类型注释由)末尾的感叹号指示(City!。这意味着capitalCity属性与任何其他可选项一样有默认值nil,但在不需要解包其值的情况下可以访问,如隐式解包可选值中所述。
因为capitalCity具有默认nil值,所以Country只要在initializer中设置name属性,就会认为新Country实例已完全初始化。这意味着Country的initializer在设置name属性后立即可以开始引用并传递隐式属性self。因此,当Country的initializer设置其自己的capitalCity属性时可以把self作为参数之一传递给City的initializer。
所有这些意味着可以在单个语句中创建Country和City实例,而不会创建强引用循环,并且可以直接访问capitalCity属性,而无需使用感叹号来解包其可选值:
ar country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
/*
打印结果:
Canada's capital city is called Ottawa
*/
上面的示例中,使用隐式解包可选项意味着满足类initializer两阶段的所有要求。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) {
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属性的命名和使用方式有点像实例方法。但是,因为asHTML是闭包属性而不是实例方法,所以如果要更改特定HTML元素的HTML呈现,则可以使用自定义闭包替换属性的默认值。
例如,如果text属性为nil,asHTML属性可以设置默认为一些文字的闭包,以防止返回一个空的HTML标签表示:
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
/*
打印结果:
<h1>some default text</h1>
*/
注意
asHTML属性被声明为一个懒加载属性,因为只有当元素实际需要呈现为某个HTML输出目标的字符串值时才需要它。asHTML作为懒加载属性的事实意味着可以在默认闭包内引用self,因为在初始化完成并且self已知存在之后才会访问懒加载属性。
HTMLElement类提供了一个单一的initializer,这需要一个name参数,和(如果需要的话)一个text参数来初始化新的元素。该类还定义了一个deinitializer,它打印一条消息,以显示HTMLElement实例何时被释放。
以下是使用HTMLElement类创建和打印新实例的方法:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
/*
打印结果:
<p>hello, world</p>
*/
注意
上面的paragraph变量被定义为可选 HTMLElement,因此下面可以将其设置为nil以证明存在强引用循环。
不幸的是,如上所述的HTMLElement类在HTMLElement实例和默认的asHTML值的闭包之间创建了一个强引用循环。这是循环的样子:
image
实例的asHTML属性强烈引用它的闭包。然而,因为闭包在主体内引用self(作为一种方式引用self.name和self.text),封闭捕获self,这意味着它对HTMLElement实例持有强引用。在两者之间创建了强大的参考循环。(有关在闭包中捕获值的更多信息,参阅捕获值。)
注意
即使闭包self多次引用,它也只捕获该HTMLElement实例的一个强引用。
如果将paragraph变量设置为nil断开对HTMLElement实例的强引用,则实例HTMLElement及其闭包都不会被释放,因为强引用循环:
paragraph = nil
请注意,HTMLElement不会打印deinitializer 中的消息,这表明HTMLElement实例未销毁。
解决闭包的强引用周期
通过将捕获列表定义为闭包定义的一部分,可以解决闭包和类实例之间的强引用循环。捕获列表定义在闭包体内捕获一个或多个引用类型时要使用的规则。与两个类实例之间的强引用循环一样,将每个捕获的引用声明为弱引用或无主引用,而不是强引用。弱引用或无主引用的适当选择取决于代码的不同部分之间的关系。
注意
Swift需要编写self.someProperty或self.someMethod()(而不仅仅是someProperty或someMethod())当在封闭内引用self的一个成员。这有助于记住,可能会self意外捕获。
定义捕获列表
捕获列表中的每项都是weak或者unowned关键字配对的类实例的引用(例如self)或使用某个值初始化的变量(例如delegate = self.delegate!)。这些配对写在一对方括号内,用逗号分隔。
将捕获列表放在闭包的参数列表和返回类型之前:
lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
// 这里是闭包主体
}
如果闭包没有指定参数列表或返回类型,因为它们可以从上下文中推断出来,那么将捕获列表放在闭包的最开头,然后是in关键字:
lazy var someClosure: () -> String = {
[unowned self, weak delegate = self.delegate!] in
// 这里是闭包主体
}
弱引用和无主引用
当闭包和它捕获的实例将始终相互引用时,将闭包中的捕获定义为无主引用,并且将始终同时销毁。
相反,当捕获的引用可能在将来的某个时刻为nil,将捕获定义为弱引用。弱引用始终是可选类型,并且在它们引用的实例被释放时自动变为nil。这使得可以检查它们在闭包体是否存在。
注意
如果捕获的引用永远不会变为nil,则应始终将其捕获为无主引用,而不是弱引用。
无主引用是用于解决上述闭包的强引用循环HTMLElement示例中的强引用循环的适当捕获方法。以下是如何编写HTMLElement类以避免循环的方法:
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]这种情况下,捕获列表意味着“将自己捕获为无主引用而非强引用”。
可以像以前一样创建和打印HTMLElement实例:
ar paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
/*
打印结果:
<p>hello, world</p>
*/
以下是引用在捕获列表中的显示方式:
image
这一次,闭包的捕获是一个无主引用self,没有对它捕获的HTMLElement实例强行持有。如果将强引用变量paragraph设置为nil,则HTMLElement实例将被释放,从下面示例中打印的deinitializer消息可以看出:
paragraph = nil
/*
打印结果:
p is being deinitialized
*/
有关捕获列表的详细信息,参阅捕获列表。