Swift中循环引用问题

本文转自https://www.cnswift.org/automatic-reference-counting

循环引用问题

因为Swift中采用ARC(自动引用计数)来管理对象实体内存,ARC的工作机制就是你每次创建一个对象实体时,ARC会在内存中创建相应的空间才存储这个对象实体,当你不再使用这个实体时,它会自动释放之前占用的内存空间以供其他实体使用。
正常情况下ARC都可以自动管理内存的分配和释放,但在特殊的情况下,需要手动处理才能使对象的实体正确释放。

下面参照Swift官方引用的例子来说明,对象之间的循环强引用问题。
这个例子定义两个类分别是Person和Apartment,用来建立公寓和其中的公民

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 { print("\(name) is being deinitialized") }
}
class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person?
    deinit { print("Apartment \(unit) is beingdeinitialized") }
}

每一个 Person 实例除了you并有一个可选的初始化为nil 的 apartment 属性。 apartment 属性是可选项,因为一个人并不总是拥有公寓。
类似的,每个 Apartment 实例都有一个叫 unit ,类型为 String 的属性,并有一个可选的初始化为 nil 的 tenant 属性。 tenant 属性是可选的,因为一栋公寓并不总是有居民。
这两个类都定义了反初始化器,用以在类实例被反初始化时输出信息。这让你能够知晓 Person 和Apartment 的实例是否像预期的那样被释放。
接下来我们定义两个可选变量john 和 unit4A,他们分别被赋值为下面两个Apartment 和 Person 的实例,这两个变量都被初始化为 nil。

1
2
3
4
5
var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

然后将两个实例关联在一起:

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

如下图:
referenceCycle02

此时这两个实体之间就形成了循环强引用,Person实例有个指向Apartment的强引用,Apartment有个指向Person实例的强引用。因此,即使你断开john和unit4A变量所持有的强引用,Person和Apartment的实例的引用计数也不会变为0,导致这两个实例无法被释放。

1
2
john = nil
unit4A = nil

引用关系如下图:
referenceCycle02
此时Person实例和Apartment实例互相强引用,他们所占的内存也无法被释放。

解决对象循环引用问题

Swift提供了两种方式来解决类属性之间的强引用问题:弱引用(weakreference)和无主引用( unowned reference )。
弱引用和无主引用都能使一个实例引用另一个实例儿不被强引用,这样能使实例互相引用而不产生循环强引用。
对于生命周期内会变成nil的实例使用弱引用,相反对于初始化后,实例不会在变为nil的实例使用无主引用。上面的 Apartment 例子中,在它的声明周期中,有时是”没有居民”的,因此适合使用弱引用来解决循环强引用。

####弱引用
弱引用不会引起对实例的强引用,因此不会阻止ARC释放被引用的实体。声明变量为弱引用只需在变量前加关键字weak来表示一个弱引用,同样使用上面例子,这次将Apartment 的 tenant 属性声明为弱引用

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 { 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 )之间的强引用和关联创建得与上次相同:

1
2
3
4
5
6
7
8
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实例使用的是弱引用,当断开变量john的强引用时,再也没有指向Person实例的强引用了,该引用就被释放了

1
2
john = nil
// prints "John Appleseed is being deinitialized"

如下图:

现在只剩下来自 unit4A 变量对 Apartment 实例的强引用。如果你打断这个强引用,那么Apartment 实例就再也没有强引用了:

1
2
unit4A = nil
// prints "Apartment 4A is being deinitialized"

####无主引用
和弱引用类似,无主引用不会牢牢保持住引用的实例。但是不想弱引用,总之,无主引用假定对象永远都有值,因此无主引用总是被定义成非可选的。你可以声明变量时,在其前面加 unowned 表示这是一个无主引用。
下面定义两个类,CustomerCreditCard 模拟客户和信用卡之间的关系,这两个类中都有将另一个类实体为自身的一个属性,这中关系可能产生循环强引用。
在这个数据模型中,一个客户可能有或者没有信用卡,但是一张信用卡总是关联着一个客户。为了表示这种关系, Customer 类有一个可选类型的 card 属性,但是 CreditCard 类有一个非可选类型的 customer 属性。
另外,新的 CreditCard 实例只有通过传送 number 值和一个 customer 实例到 CreditCard 的初始化器才能创建。这就确保了 CreditCard 实例在创建时总是有与之关联的 customer 实例。
由于信用卡总是关联着一个客户,因此将 customer 属性定义为无主引用,以避免循环强引用:

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

下面的代码片段定义了一个叫 john 的可选 Customer 变量,用来保存某个特定客户的引用。由于是可选项,所以变量被初始化为 nil 。

1
2
3
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
2
3
john = nil
// prints "John Appleseed is being deinitialized"
// prints "Card #1234567890123456 is being deinitialized"

闭包的循环引用

下面的例子为你展示了当一个闭包引用了 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 {
        print("\(name) is being deinitialized")
    }
    
}

HTMLElement 类定义了一个 name 属性来表示这个元素的名称,例如表示标题元素的 “h1” 、代表段落元素的 “p” 、或者代表换行元素的 “br” 。 HTMLElement 还定义了一个可选的属性text ,它可以用来设置和展现 HTML 元素的文本。
除了上面的两个属性, HTMLElement 还定义了一个 lazy 属性 asHTML 。这个属性引用了一个将name 和 text 组合成 HTML 字符串片段的闭包。该属性是 Void -> String 类型,或者可以理解为“一个没有参数,但返回 String 的函数”。
默认情况下,闭包赋值给了 asHTML 属性,这个闭包返回一个代表 HTML 标签的字符串。如果text 值存在,该标签就包含可选值 text ;如果 text 不存在,该标签就不包含文本。对于段落元素,根据 text 是 “some text” 还是 nil ,闭包会返回"<p>some text</p>"或者 "<p />"
可以像实例方法那样去命名、使用 asHTML 属性。总之,由于 asHTML 是闭包而不是实例方法,如果你想改变特定元素的 HTML 处理的话,可以用自定义的闭包来取代默认值。

1
2
3
4
5
6
7
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>"

HTMLElement 类只提供一个初始化器,通过 name 和 text (如果有的话)参数来初始化一个元素。该类也定义了一个初始化器,当 HTMLElement 实例被释放时打印一条消息。
下面的代码展示了如何用 HTMLElement 类创建实例并打印消息。

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

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

实例的 asHTML 属性持有闭包的强引用。但是,闭包在其闭包体内使用了 self (引用了self.name 和 self.text ),因此闭包捕获了 self ,这意味着闭包又反过来持有了HTMLElement 实例的强引用。这样两个对象就产生了循环强引用。(更多关于闭包捕获值的信息,请参考值捕获)。
如果设置 paragraph 变量为 nil ,打破它持有的 HTMLElement 实例的强引用,HTMLElement 实例和它的闭包都不会被释放,也是因为循环强引用:

1
paragraph = nil

注意 HTMLElement 的反初始化器中的消息并没有被打印,证明了 HTMLElement 实例并没有被销毁。

###解决闭包的循环强引用
你可以通过定义捕获列表作为闭包的定义来解决在闭包和类实例之间的循环强引用。捕获列表定义了当在闭包体里捕获一个或多个引用类型的规则。正如在两个类实例之间的循环强引用,声明每个捕获的引用为引用或无主引用而不是强引用。应当根据代码关系来决定使用弱引用还是无主引用。
在闭包和捕获的实例总是互相引用并且总是同时释放时,将闭包内的捕获定义为无主引用。
相反,在被捕获的引用可能会变为 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 {
        print("\(name) is being deinitialized")
    }
    
}

上面的 HTMLElement 实现和之前的实现一致,除了在 asHTML 闭包中多了一个捕获列表。这里,捕获列表是 [unowned self] ,表示“用无主引用而不是强引用来捕获 self ”。
和之前一样,我们可以创建并打印 HTMLElement 实例:

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

使用捕获列表后引用关系如下图所示:

这次,闭包以无主引用的形式捕获 self ,并不会持有 HTMLElement 实例的强引用。如果将paragraph 赋值为 nil , HTMLElement 实例将会被释放,并能看到它的反初始化器打印出的消息。

1
2
paragraph = nil
// prints "p is being deinitialized"