Swift的ARC机制

Swift中的ARC机制和Objective-C类似。每次创建一个类的新的实例的时候,ARC会分配一块内存在存储该实例信息,内存中会包含实例的类型信息以及这个实例所有相关的存储型属性的值。当实例不再被使用时,ARC会释放所占用的内存。

鉴定一个实例是否被释放的方法:
在析构函数中添加打印信息,若实例被置为nil后,没有打印析构函数信息,则实例仍然存在引用,没有被销毁。

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

根据使用场景不同有三种解决方法:

  • 弱引用
  • 无主引用
  • 无主引用与隐式解析可选属性结合

弱引用和无主引用都不会保持所引用的实例,从而实现在循环引用中的一个实例引用而另一个实例不保持强引用。

弱引用和无主引用区别:

  • 弱引用类型的属性或变量为可选类型变量,ARC会在被引用的实例被销毁后自动将其赋值为nil。弱引用允许它们的值在运行时被赋值为nil
  • 无主引用类型的属性或变量为非可选类型变量不允许被赋值为nil。ARC无法在实例被销毁后将无主引用设为nil。
  • 无主引用必须确保引用始终指向一个未销毁的实例

使用场景区别:

  • 两个相互引用的属性的值都允许为nil,并会潜在的产生循环强引用。这种场景最适合用弱引用来解决。

  • 两个互相引用的属性中,一个属性的值允许为nil,而另一个属性的值不允许为nil,这也可能会产生循环强引用。这种场景最适合通过无主引用来解决。

  • 当两个互相引用的属性都必须有值,并且初始化完成后永远不会为nil。
    需要一个类使用无主属性,另一个类使用隐式解析可选属性。

使用示例

  1. 弱引用—— weak reference

当其他的实例有更短的生命周期也就是其他实例析构在先时,使用弱引用。弱引用类型的属性或变量的值可以为nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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")

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

Person实例保持对Apartment实例的强引用,但是Apartment实例只持有对Person实例的弱引用。

  1. 无主引用—— unowned refrence

当其他实例有相同的或更长生命周期时,使用无主引用。无主引用的属性或变量必须有值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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!)

Customer实例持有对CreditCard实例的强引用,而CreditCard实例持有对Customer实例的无主引用。由于customer的无主引用,当你断开john变量持有的强引用时,再也没有指向Customer实例的强引用了。由于再也没有指向Customer实例的强引用,该实例被销毁了。其后,再也没有指向CreditCard实例的强引用,该实例也随之被销毁了。

  1. 无主引用和隐式解析可选属性

两个属性在初始化完成后能被直接访问(不需要可选展开),同时避免了循环引用。

每个国家必须有首都,每个城市必须属于一个国家。为了实现这种关系,Country类拥有一个capitalCity属性,而City类有一个country属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Country {
let name: String
/*
通过在类型结尾处加上感叹号(City!)的方式,将Country的capitalCity
属性声明为隐式解析可选类型的属性。这意味着像其他可选类型一样,
capitalCity属性的默认值为nil,但是不需要展开它的值就能访问它。
*/
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
}
}

/*
Country的构造函数调用了City的构造函数。只有Country的实例完全初始化后,
Country的构造函数才能把self传给City的构造函数
*/

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

闭包引起的循环强引用

使用闭包时循环强引用的产生,是因为闭包和类相似,都是引用类型。当你把一个闭包赋值给某个属性时,你是将这个闭包的引用赋值给了属性。

Swift提供了一种优雅的方法来解决这个问题,称之为闭包捕获列表(closure capture list)

循环强引用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class HTMLElement {

let name: String
let text: String?

lazy var asHTML: Void -> 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")
}
}

实例的asHTML属性持有闭包的强引用。但是,闭包在其闭包体内使用了self(引用了self.nameself.text),因此闭包捕获了self,这意味着闭包又反过来持有了HTMLElement实例的强引用。这样两个对象就产生了循环强引用。

说明:asHTML声明为lazy属性,因为只有当元素确实需要被处理为HTML输出的字符串时,才需要使用asHTML。也就是说,在默认的闭包中可以使用self,因为只有当初始化完成以及self确实存在后,才能访问lazy属性。

解决闭包循环引用

通过在定义闭包时同时定义捕获列表作为闭包的一部分来解决。捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,声明每个捕获的引用为弱引用或无主引用,而不是强引用。应当根据代码关系来决定使用弱引用还是无主引用。

Swift 有如下要求:只要在闭包内使用self的成员,就要用self.someProperty或者self.someMethod()(而不只是someProperty或someMethod())。这提醒你可能会一不小心就捕获了self。

捕获列表中的每一项都由一对元素组成,一个元素是weak或unowned关键字,另一个元素是类实例的引用(例如self)或初始化过的变量(如delegate = self.delegate!)。这些项在方括号中用逗号分开。

如果闭包有参数列表和返回类型,把捕获列表放在它们前面:

1
2
3
4
lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
// 这里是闭包的函数体
}

如果闭包没有指明参数列表或者返回类型,即它们会通过上下文推断,那么可以把捕获列表和关键字in放在闭包最开始的地方:

1
2
3
4
lazy var someClosure: Void -> String = {
[unowned self, weak delegate = self.delegate!] in
// 这里是闭包的函数体
}
  • 在闭包和捕获的实例总是互相引用并且总是同时销毁时,将闭包内的捕获定义为无主引用。

  • 在被捕获的引用可能会变为nil时,将闭包内的捕获定义为弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。这使我们可以在闭包体内检查它们是否存在。

  • 如果被捕获的引用绝对不会变为nil,应该用无主引用,而不是弱引用。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class HTMLElement {

let name: String
let text: String?

lazy var asHTML: Void -> 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())
// 打印 “<p>hello, world</p>”

paragraph = nil
// 打印 “p is being deinitialized”