引入
Swift使用自动引用计数(ARC)来跟踪和管理应用程序的内存使用情况。 在大多数情况下, 这意味着内存管理在Swift中 “只需工作”, 你无需亲自考虑内存管理问题。 当不再需要类实例时, ARC 会自动释放这些实例使用的内存。
不过在某些情况下, ARC 需要关于代码各部分之间关系的更多信息,才能为您管理内存。
引用计数仅适用于类的实例。结构和枚举是值类型,而不是引用类型,并且不通过引用存储和传递。
ARC如何工作
每次创建一个类的新实例时, ARC都会分配一块内存来存储该实例的信息。 这段内存存储了实例的类型信息, 以及与实例相关的任何存储属性的值。
当不再需要某个实例时, ARC会释放该实例使用的内存, 以便将内存用于其他目的。 这确保了类实例在不再需要时不会占用内存空间。
如果ARC将一个仍在使用的实例销毁, 就无法再访问该实例的属性或调用该实例的方法。 事实上,如果你试图访问该实例, 您的应用程序很可能会崩溃。
为了确保实例不会在需要时消失, ARC会跟踪当前有多少属性、 常量和变量引用了每个类的实例。 只要至少有一个实例的有效引用还存在, ARC就不会销毁该实例。
为了做到这一点, 每当你将一个类实例分配给一个属性、 常量或变量时, 该属性、常量或变量都会对该实例进行强引用。 该引用之所以被称为 “强 ”引用, 是因为它牢牢地控制着该实例,只要该强引用仍然存在,就不允许将其销毁。
ARC示例
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var reference1: Person?
var reference2: Person?
var reference3: Person?
reference1 = Person(name: "John Appleseed") // reference1强引用
// Prints "John Appleseed is being initialized"
// 实例又多了两个强引用
reference2 = reference1
reference3 = reference1
// 将其中两个强引用断开, 实例不会销毁
reference1 = nil
reference2 = nil
// 最后一个强引用断开以后, 实例被销毁
reference3 = nil
// Prints "John Appleseed is being deinitialized"
类实例之间的强引用循环
在上面的示例中, ARC可以跟踪您创建的新 Person 实例的引用数量, 并在不再需要该 Person 实例时将其删除。
在编写代码时, 有可能一个类的实例永远不会达到零强引用的程度。 如果两个类实例相互持有强引用, 从而使每个实例都能保持另一个实例的活力, 就会出现这种情况。 这就是所谓的强引用循环。
解决强引用循环的方法是, 将类之间的某些关系定义为弱引用或无所有引用, 而不是强引用。
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") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
// 将两个实例连接起来,它们就构成了强引用循环
john!.apartment = unit4A
unit4A!.tenant = john
// 打断变量持有的强引用, 实例也不会被销毁, 这样就导致了内存泄漏
john = nil
unit4A = nil
解决类实例之间的强引用循环
在处理类类型的属性时, Swift提供了两种解决强引用循环的方法: 弱引用和非自有引用。
弱引用和非自有引用使引用循环中的一个实例可以引用另一个实例, 而无需对其保持强控制。 这样, 实例之间就可以相互引用, 而不会形成一个强引用循环。
当另一个实例的生命周期较短时, 也就是当另一个实例可以首先被删除时, 请使用弱引用。 在上面的 “公寓 ”示例中, “公寓 ”在其生命周期的某个阶段没有租户是合适的, 因此在这种情况下, 弱引用是打破引用循环的合适方法。 相反,当其他实例具有相同的生命周期或更长的生命周期时, 应使用非自有引用。
弱引用
弱引用是一种不会对其引用的实例进行强力控制的引用, 因此不会阻止 ARC 处置所引用的实例。 这种行为可以防止引用成为强引用循环的一部分。 你可以在属性或变量声明前使用 weak 关键字 来表示弱引用。
由于弱引用不会对其引用的实例进行强力控制, 因此当弱引用仍在引用该实例时, 该实例就有可能被销毁。 因此, 当弱引用所引用的实例被销毁时, ARC会自动将其设置为 nil。 此外, 由于弱引用需要允许在运行时将其值更改为 nil, 因此它们总是被声明为变量, 而不是常量, 属于可选类型。
你可以检查弱引用中的值是否存在, 就像检查其他可选值一样,你永远不会得到一个不再存在的无效实例的引用。
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") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
// person实例对apartment实例强引用,apartment对person实例是弱引用
john!.apartment = unit4A
unit4A!.tenant = john
// 没有强引用指向join,所以被销毁
john = nil
// Prints "John Appleseed is being deinitialized"
// 没有强引用指向unit4A实例, 也被销毁
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
无主引用
与弱引用一样, 非自有引用不会对其所引用的实例进行强力控制。 但与弱引用不同的是,
当其他实例具有相同或更长的生命周期时, 就会使用非自有引用。 你可以在属性或变量声明前使用
unowned
关键字来表示 unowned 引用。
与弱引用不同的是, 非自有引用总是有一个值。 因此, 将一个值标记为 unowned 并不能使其成为可选项, 而且 ARC 从不将 unowned 引用的值设置为 nil。
下面的示例定义了 Customer 和 CreditCard 这两个类,它们分别是银行客户和该客户可能使用的信用卡的模型。这两个类分别存储了另一个类的一个实例作为属性。这种关系有可能产生强大的引用循环。
客户与信用卡之间的关系与上述弱引用示例中公寓与人之间的关系略有不同。在此数据模型中,客户可以有信用卡,也可以没有,但信用卡始终与客户相关联。信用卡实例的寿命永远不会超过它所引用的 “客户”。为了表示这一点,“客户 ”类有一个可选的卡属性,但 “信用卡 ”类有一个非自有(非可选)的 “客户 ”属性。
此外,新的信用卡实例只能通过向自定义信用卡初始化程序传递一个数字值和一个客户实例来创建。这确保了在创建 CreditCard 实例时,该实例总是与一个客户实例相关联。
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") }
}
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
无主可选引用
可以将一个类的可选引用标记为非拥有引用。 就 ARC 所有权模型而言, 无所有权的可选引用和弱引用可以在相同的上下文中使用。 不同之处在于, 当你使用非所有的可选引用时, 你有责任确保它始终指向一个有效的对象, 或者将其设置为零。
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
}
}
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]
闭包的强引用循环
在上文中, 我们看到了当两个类的实例属性相互持有强引用时, 如何创建强引用循环。 你还看到了如何使用弱引用和非所有引用来打破这些强引用循环。
如果你为类实例的一个属性指定了一个闭包, 而该闭包的主体捕获了该实例, 也会产生强引用循环。 这种捕获可能是因为闭包的主体访问了实例的一个属性, 如 self.someProperty, 也可能是因为闭包调用了实例的一个方法, 如 self.someMethod()。 无论哪种情况, 这些访问都会导致闭包 “捕获 ”self,从而创建一个强引用循环。
之所以会出现这种强引用循环, 是因为闭包和类一样, 都是引用类型。 当你将一个闭包赋值给一个属性时, 就等于将一个引用赋值给了该闭包。从本质上讲, 这和上面的问题是一样的—两个强引用彼此保持着活力。 不过,这次不是两个类实例, 而是一个类实例和一个闭包在互相维持生命。
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")
}
}
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>"
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
paragraph = nil
解决闭包中的强引用循环
通过定义捕获列表作为闭包定义的一部分, 可以解决闭包与类实例之间的强引用循环问题。 捕获列表定义了在闭包主体中捕获一个或多个引用类型时使用的规则。 与两个类实例之间的强 引用循环一样, 每个捕获的引用都要声明为弱引用或无所有引用, 而不是强引用。 弱引用或非自有引用的适当选择取决于代码不同部分之间的关系。
定义捕获列表
捕获列表中的每一项都是弱关键字或非自有关键字与类实例引用(如 self)或某个值初始化变量(如 delegate = self.delegate)的配对。这些配对写在一对方括号中,中间用逗号隔开。
如果提供了闭包的参数列表和返回类型,请将捕获列表放在它们之前。
lazy var someClosure = {
[unowned self, weak delegate = self.delegate]
(index: Int, stringToProcess: String) -> String in
// closure body goes here
}
弱引用和无主引用
将闭包中的捕获定义为无主引用时,闭包和它捕获的实例将始终相互引用,并将始终同时被解除分配。
相反,当捕获的引用可能在未来某个时刻变为 nil 时,则将捕获定义为弱引用。弱引用始终属于可选类型,并在其引用的实例被解除分配后自动变为 nil。这样就可以在闭包的主体中检查弱引用是否存在。
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")
}
}
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
paragraph = nil
// Prints "p is being deinitialized"