文章目录
  1. 1. 简介
  2. 2. 类实例之间强引用循环的产生
  3. 3. 如何解决实例间强引用循环
    1. 3.1. 弱引用
    2. 3.2. 无主引用
    3. 3.3. 无主引用以及隐式解析可选属性
    4. 3.4. 总结
  4. 4. 闭包引起的强引用循环
  5. 5. 解决闭包引起的循环强引用
    1. 5.1. 定义捕获列表
    2. 5.2. 捕获列表中的弱引用和无主引用

Objective-C中的ARC被Swift很好的继承下来了,本文参考自Swift文档网上的翻译,主要重点记录下Swift中的ARC与OC对比需要注意的地方。(2014-8-8更新至beta5语法)

简介

当你每次创建一个类的新的实例的时候,ARC 会分配一大块内存用来储存实例的信息。内存中会包含实例的类型信息,以及这个实例所有相关属性的值。此外,当实例不再被使用时,ARC 释放实例所占用的内存,并让释放的内存能挪作他用。这确保了不再被使用的实例,不会一直占用内存空间。

类实例之间强引用循环的产生

如果你对OC中的强引用循环很了解,可以直接跳过这节。

但是如果两个类的实例之间互相引用,这样就产生了强引用循环。下面展示了一个不经意产生强引用循环的例子。例子定义了两个类:PersonApartment,用来建模公寓和它其中的居民:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { println("\(name) is being deinitialized") }
}

class Apartment {
let number: Int
init(number: Int) { self.number = number }
var tenant: Person?
deinit { println("Apartment #\(number) is being deinitialized") }
}

接下来的代码片段定义了两个可选类型的变量johnnumber73,并分别被设定为下面的ApartmentPerson的实例。这两个变量都被初始化为nil,并为可选的(让它们可选是为了以后能销毁,为了演示程序):

1
2
var john: Person?
var number73: Apartment?

现在你可以创建特定的PersonApartment实例并将类实例赋值给johnnumber73变量:

1
2
john = Person(name: "John Appleseed")
number73 = Apartment(number: 73)

johnnumber73互相引用之前,它们的强引用关系是这样的:

现在你能够将这两个实例关联在一起,这样人就能有公寓住了,而公寓也有了房客。注意感叹号是用来强制解析可选变量johnnumber73中的实例:

1
2
john!.apartment = number73
number73!.tenant = john

在将两个实例联系在一起之后,强引用的关系变成了这样:

这样即使让johnnumber73断开它们持有的强引用,内存中的那两个PersonApartment实例并不会销毁,因为它们互相引用,引用计数都为1:

1
2
john = nil
number73 = nil

当你把这两个变量设为nil时,没有任何一个析构函数被调用。强引用循环阻止了PersonApartment类实例的销毁,并在你的应用程序中造成了内存泄漏。

如何解决实例间强引用循环

弱引用

跟OC中的弱引用相似,声明属性或者变量时,在前面加上weak关键字表明这是一个弱引用。弱引用不会牢牢保持住引用的实例,并且不会阻止 ARC 销毁被引用的实例。因为弱引用的值会变化并可能为nil,所以弱引用不能是常量,必须是可选类型(Optional)。因为弱引用不会保持所引用的实例,即使引用存在,实例也有可能被销毁。因此,ARC 会在引用的实例被销毁后自动将其赋值为nil。

下面的例子跟上面PersonApartment的例子一致,但是有一个重要的区别。这一次,Apartmenttenant属性被声明为弱引用:

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { println("\(name) is being deinitialized") }
}
class Apartment {
let number: Int
init(number: Int) { self.number = number }
weak var tenant: Person?
deinit { println("Apartment #\(number) is being deinitialized") }
}

然后跟之前一样,建立两个变量(johnnumber73)之间的强引用,并关联两个实例:

1
2
3
4
5
6
7
8
var john: Person?
var number73: Apartment?

john = Person(name: "John Appleseed")
number73 = Apartment(number: 73)

john!.apartment = number73
number73!.tenant = john

现在的引用关系如下:

john的强引用断开后,引用关系变成了这样子:

因为没有强引用指向Person实例,它的引用计数为0,所以该实例会被销毁。因此number73指向的Apartment实例的的引用计数会变为1,因为Person实例销毁后,其apartment属性对Apartment实例的强引用也会断开。此时如果再断开number73Apartment实例的强引用:

Apartment实例因为引用计数为0,会被销毁,到此为止强引用循环被打破。

无主引用

和弱引用类似,无主引用不会牢牢保持住引用的实例。和弱引用不同的是,无主引用是永远有值的(不能为nil)。因此,无主引用总是被定义为非可选类型(non-optional type)。你可以在声明属性或者变量时,在前面加上关键字unowned表示这是一个无主引用。

下面的例子定义了两个类,CustomerCreditCard,模拟了银行客户和客户的信用卡。一个客户可能有或者没有信用卡,但是一张信用卡总是关联着一个客户。所以Customer类的card属性可以为nil,但是CreditCard类的customer属性不能为nil,所以创建CreditCard实例的时候必须给customer属性赋值避免其为nil。将customer属性定义为无主引用,用以避免循环强引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { println("\(name) is being deinitialized") }
}
class CreditCard {
let number: Int64
unowned let customer: Customer
init(number: Int64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { println("Card #\(number) is being deinitialized") }
}
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

关联两个实例后,引用关系如下:

由于customer的无主引用,当你断开john变量持有的强引用时,再也没有指向Customer实例的强引用了:

于是Customer实例被销毁,这样又导致没有强引用指向CreditCard实例,最后CreditCard实例也被销毁了,这样说明强引用循环被打破了。

无主引用以及隐式解析可选属性

下面的例子定义了两个类,CountryCity,每个类将另外一个类的实例保存为属性。在这个模型中,每个国家必须有首都,而每一个城市必须属于一个国家。为了实现这种关系,Country类拥有一个capitalCity属性,而City类有一个country属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Country {
let name: String
let 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
}
}

在这种场景中,两个属性都必须有值,并且初始化完成后不能为nil。在这种场景中,需要一个类使用无主属性,而另外一个类使用隐式解析可选属性。

隐式解析可选(Implicitly Unwrapped Optionals)被用于类的初始化方法中,避免循环引用,因为编译器会将其认为默认nil,不需赋值就可以完成类的初始化,在例子中一旦name属性被赋值后(但capitalCity还没被赋值时)Country类就已经初始化并且自身(self)可以被引用;这样就能实现一行代码建立CountryCity实例而不造成强引用循环。

1
2
3
var country = Country(name: "Canada", capitalName: "Ottawa")
println("\(country.name)'s capital city is called \(country.capitalCity.name)")
// prints "Canada's capital city is called Ottawa"

使用隐式解析可选值的意义在于满足了两个类构造函数的需求。capitalCity属性在初始化完成后,能像非可选值一样使用和存取同时还避免了循环强引用。

总结

PersonApartment的例子展示了两个属性的值都允许为nil,并会潜在的产生循环强引用。这种场景最适合用弱引用来解决。

CustomerCreditCard的例子展示了一个属性的值允许为nil,而另一个属性的值不允许为nil,并会潜在的产生循环强引用。这种场景最适合通过无主引用来解决。

CountryCity的例子展示了两个属性的值都不允许为nil,并会潜在的产生循环强引用。这种场景需要一个类使用无主属性,而另外一个类使用隐式解析可选属性。

闭包引起的强引用循环

强引用循环还会发生在当你将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了实例。这个闭包体中可能访问了实例的某个属性,例如self.someProperty,或者闭包中调用了实例的某个方法,例如self.someMethod。这两种情况都导致了闭包 “捕获” self,因为闭包也是引用类型,从而产生了强引用循环。在Swift中闭包如果想使用外部的实例,不必像OC中的Block那样在外部实例前加__block加以修饰,而是可以直接用“值捕获”的方式捕获到闭包外面的实例。Swift 会决定捕获引用还是拷贝值,并负责管理内存释放。

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

在OC中也存在Block中引用selfself的属性而导致selfBlock retain,进而产生引用循环,这也是为什么代理属性都被声明为weak的原因。

下面的例子为你展示了当一个闭包引用了self后是如何产生一个循环强引用的。例子中定义了一个叫HTMLElement的类,用一种简单的模型表示 HTML 中的一个单独的元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 {
println("\(name) is being deinitialized")
}

}

HTMLElement定义了一个lazy属性asHTML。这个属性引用了一个闭包,将nametext组合成 HTML 字符串片段。该属性是() -> String类型,或者可以理解为“一个没有参数,返回String的函数”。因为该闭包无参数并可推断出返回值类型,所以采取了简写,省略了关键字in和闭包的参数和返回值类型声明。

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

下面的代码展示了如何用HTMLElement类创建实例并打印消息

1
2
3
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
println(paragraph!.asHTML())
// prints"hello, world"

不幸的是,上面写的HTMLElement类产生了类实例和asHTML默认值的闭包之间的循环强引用。循环强引用如下图所示:

虽然闭包多次使用了self,它只捕获HTMLElement实例的一个强引用。如果设置paragraph变量为nil,打破它持有的HTMLElement实例的强引用,HTMLElement实例和它的闭包都不会被销毁,也是因为强引用循环

解决闭包引起的循环强引用

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

定义捕获列表

捕获列表放置在闭包参数列表和返回类型之前,列表中每项都是由weakunowned关键字和实例的引用(如self或someInstance)成对组成。每项都通过逗号分开写在方括号中。

1
2
3
4
lazy var someClosure: (Int, String) -> String = {
[unowned self] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}

如果闭包没有指定参数列表或者返回类型,则可以通过上下文推断,那么可以捕获列表放在闭包开始的地方,跟着是关键字in

1
2
3
4
lazy var someClosure: () -> String = {
[unowned self] in
// closure body goes here
}

捕获列表中的弱引用和无主引用

当捕获引用有时可能会是nil时,将闭包内的捕获定义为弱引用。
当闭包和捕获的实例总是互相引用时并且总是同时销毁时,将闭包内的捕获定义为无主引用。
如果捕获的引用绝对不会置为nil,应该用无主引用,而不是弱引用。

前面的HTMLElement例子中,无主引用是正确的解决循环强引用的方法。这样编写HTMLElement类来避免循环强引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 {
println("\(name) is being deinitialized")
}

}

上面的例子只是多了一个捕获列表并增加关键字in,使用捕获列表后引用关系如下图所示:

这一次,闭包以无主引用的形式捕获self,并不会持有HTMLElement实例的强引用。如果将paragraph赋值为nilHTMLElement实例将会被销毁,并能看到它的析构函数打印出的消息。

1
2
paragraph = nil
// prints "p is being deinitialized"
文章目录
  1. 1. 简介
  2. 2. 类实例之间强引用循环的产生
  3. 3. 如何解决实例间强引用循环
    1. 3.1. 弱引用
    2. 3.2. 无主引用
    3. 3.3. 无主引用以及隐式解析可选属性
    4. 3.4. 总结
  4. 4. 闭包引起的强引用循环
  5. 5. 解决闭包引起的循环强引用
    1. 5.1. 定义捕获列表
    2. 5.2. 捕获列表中的弱引用和无主引用