[译]Swift中的Weak,Strong和Unowned

我经常发现在我的代码中会出现循环引用的情况。我觉得别人也会出现这样的情况。虽然我不认识你,但是我相信在耳边肯定会经常出现“我什么时候要使用’weak’?如果我用’unowned’会有什么坑?”这篇文章我将会在其中写明如何在代码中使用strong, weak, unowned从而避免循环引用。但是我们可能都不知道如何使用他们。幸运的是,我将会告诉你他们都是些什么,什么时候应该是用他们。我希望这篇文章能够帮助你学习在什么情况下需要使用它们。

让我们开始吧

ARC

ARC 是一个苹果用来自动管理内存的技术。他具体体现在 Automatic Reference Counting.(自动引用计数) 这意味着一个对象只有在没有强引用的时候才会被释放。

STRONG

让我们从什么是 Strong 开始吧。这本质上是一个很普通的引用(包括指针和其他的所有引用方法。)但是他的特殊点在于它能够 保护 被引用的对象在引用计数中一直有1个引用计数,从而防止被释放。但究其本质,和其他的别的一样 就是对一个对象有着强引用,这使得对象不被释放。这一点在接下去介绍循环引用的时候很重要。
强引用在 swift 中随处可见。实际上, Strong 是 swift 中的默认属性。对于内联对象中我们使用 strong 是安全的。对于一个对象的成员变量,一般我们也是使用强引用类型。

接下去就是一个强引用的例子

1
2
3
class Kraken { let tentacle = Tentacle() //strong reference to child.}
class Tentacle { let sucker = Sucker() //strong reference to child}
class Sucker {}

在上面的内敛代码中,Kraken有对于Tentacle的强引用,其中Tentacle对于Sucker有着强引用。这种强引用了从父对象Kraken流向了子对象Sucker(也就是说引用是连续的)
相同的,在动画的块中,引用关系也是相同的。

1
UIView.animateWithDuration(0.3) { self.view.alpha = 0.0}

上面的代码中,animationWIthDuration是一个UIView的静态方法,其中的闭包块是父亲,而其中的self是孩子。

如果孩子当中也要引用父亲对象的话需要怎么办?接下来我们就要使weakUnowned属性。

WEAK 和 UNOWNED

WEAK

weak 引用只是一个指向那个对象的指针,但是他不会保护这个对象,所以当没有对象持有他的时候,他将会被 ARC 释放。如果这个对象被强引用,那么他的引用计数会加一,但是弱引用则不会。例外,弱引用的指针也会在对象成功释放的时候置零。这个时候如果你再去获得他的对象,会获得一个无效的对象或者nil。

在 swift 中,所有的弱引用都会作为Optionals(考虑一下 varlet的区别)因为这个引用应该 并且 将会 在没有对象持有他的时候置为nil。

接下来是个例子,代码不能进行编译。

1
class Kraken { weak let tentacle = Tentacle() //let is a constant! All weak variables MUST be mutable.}

因为前缀是let。而let会使得这个对象在运行时(runtime)的情况下无法变化。因为weak的前缀的对象可能会使 nil ,所以 swift 的编译器中对于弱引用的变量需要使用var

weak的弱引用主要使用的地方是用在你觉得有可能会出现 循环引用 的地方。一个循环发生在两个对象都 强引用 引用彼此的时候。如果两个对象都强引用着彼此的时候,ARC 将不会调用 release 的代码,因为两者都彼此没有释放。下图是苹果用来解释循环引用的插图。

循环引用

一个完美的体现循环引用的函数(相当新的)就是 NSNotification 的 APIs。让我们来看下下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Kraken {
var notificationObserver: ((NSNotification) -> Void)?
init() {
notificationObserver = NSNotificationCenter.defaultCenter().addObserverForName("humanEnteredKrakensLair", object: nil, queue: NSOperationQueue.mainQueue()) { notification in
self.eatHuman()
}
}
deinit {
if notificationObserver != nil {
NSNotificationCenter.defaultCenter.removeObserver(notificationObserver)
}
}
}

代码中产生了循环引用。正如你所看到的,闭包就像 Objective-C 中的 block 。如果任何变量声明在 闭包范围之外 ,引用 该范围内 的该变量将创建另一个对该对象的强引用。唯一的例外是使用价值语义的变量,例如 Swift 中的 Ints , Strings , Arrays 和 Dictionaries。

这里 NSNotificationCenter retain 了一个闭包并且在闭包中在调用 eatHuman 持有了 self 这个强引用。上面的代码中你在deinit方法中释放了消息通知这个成员变量,但是deinit不会被 ARC 调用,因为这个闭包里面调用了 Kraken 自身这个强应用。

另外的情况也会发生在 NSTimers 和 NSThread 里面。

而解决这个的办法就是对于闭包中的self这个对象使用弱引用进行引用。这将会打破强引用的循环。这样的话,我们的对象引用关系将会如下图所示:
weak & strong

改变成弱引用,不会增加弱引用的引用计数器,因此 ARC 将会在正确的时候将其释放。

如果需要在闭包中使用weakunowned,你需要使用中括号([])在闭包的内部。

例子如下:

1
2
3
let closure = { [weak self] in
self?.doSomething() //Remember, all weak variables are Optionals!
}

为什么要在使用 weak 的时候使用中括号语法?这看起来十分诡异!在 swift 中我们只有在数组中看到中括号。让我们猜一下?你可以在闭包中指定多个捕获值。
例子如下:

1
2
3
4
let closure = { [weak self, unowned krakenInstance] in //Look at that sweet Array of capture values.
self?.doSomething() //weak variables are Optionals!
krakenInstance.eatMoreHumans() //unowned variables are not.
}

这看起来和数组很像对吧?现在你知道为什么捕获值在方括号中。所以现在让我们继续学习,我们能够修复消息中的代码的循环引用了。我们可以将[weak self]加入闭包的捕获列表中。

1
2
3
NSNotificationCenter.defaultCenter().addObserverForName("humanEnteredKrakensLair", object: nil, queue: NSOperationQueue.mainQueue()) { [weak self] notification in //The retain cycle is fixed by using capture lists!
self?.eatHuman() //self is now an optional!
}

另外我们需要使用弱引用的时候就是在 使用协议的使用委托中。在 swift 中,因为类使用引用语义。在 swift 中,结构体和枚举类型也能遵守协议。但是他们使用引用语义。如果一个父类使用了代理,那么他的成员变量如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Kraken: LossOfLimbDelegate {
let tentacle = Tentacle()
init() {
tentacle.delegate = self
}
func limbHasBeenLost() {
startCrying()
}
}
protocol LossOfLimbDelegate {
func limbHasBeenLost()
}
class Tentacle {
var delegate: LossOfLimbDelegate?
func cutOffTentacle() {
delegate?.limbHasBeenLost()
}
}

然后你需要使用 weak。在这个例子中 Tentacle 通过代理强引用了 Kraken,而在 Kraken 中使用了一起强引用了他的成员变量 Tentacle。我们通过弱引用,使得他的代理定义如下:

1
weak var delegate:LossOfLimbDelegate?

大声告诉我你想说啥?做了这个你就不能编译了?!好吧,这个问题是因为一个没有类的类型的代理不能够使用 weak 来进行表示。

为了解决这个问题,我们不得不使用一个class协议来修饰我们的代理就像我们协议的内敛:class来进行修饰:

1
2
3
protocol LossOfLimbDelegate: class { //The protocol now inherits class
func limbHasBeenLost()
}

我们什么时候需要使用:class,苹果给出的解释如下:

”当该协议的要求定义的行为假定或要求符合类型具有引用语义而不是值语义时,请使用类专用协议”

本质上,如果你使用了有层次结构的引用,你使用:class。但是对于结构体和枚举类型,你就不需要使用:class,因为结构体和枚举类型是值类型而类是引用类型。

UNOWNED

WeakUnowned引用很像,但是他们还是不一样的。所以说 Unowned 引用和 Weak 引用很像。Unowned 不会增加引用计数,在 Swift 中,Unowned 引用意味着对应的类型 不可能为 Optional 类型。这使得人们更简单的去管理而不需要重新去判断是否是一个optoin类型。这不像这里面所说的。另外,Unowned引用是不会置零。这意味着当对象呗释放的时候,他不会不会亲历指针。这意味着使用 Unowned 会导致野指针的存在。对于像我一样已经用过 Objective-C 的人来说,unowned映射到 Objective-C 上面就像 unsafe_unretained

现在你可能过一点疑惑。weak 和 unowned 这两个都不会增加引用计数,但是他们都会解决循环引用的问题,那么我们需要在什么时候使用他们呢?!苹果的文档是这么介绍的:

weak 在某个生命周期中的某个时刻是有效的。但是如果你知道设置了这个变量后,他永远都不会为0,那么请使用 unowned

那你读过这类文章,就像这篇文章。如果你能够保证引用不会置空,那么请使用 unowned 否则请使用 weak。

这里有一个很好的例子,里面的闭包里面存在循环引用。但是其中的 self 引用永远不可能为空:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class RetainCycle {
var closure: (() -> Void)!
var string = "Hello"
init() {
closure = {
self.string = "Hello, World!"
}
}
}
//Initialize the class and activate the retain cycle.
let retainCycleInstance = RetainCycle()
retainCycleInstance.closure() //At this point we can guarantee the captured self inside the closure will not be nil. Any further code after this (especially code that alters self's reference) needs to be judged on whether or not unowned still works here.

在这种情况下,循环引用是因为闭包中捕获了了 self 并强引用了他,并将其作为自己的一个闭包属性。为了打破这个循环引用,我们使用[unowned self]
加入闭包中的结果如下:

1
2
3
closure = { [unowned self] in
self.string = "Hello, World!"
}

在这种情况下,我们能够保证 self 永远不为空,在我们创建了 循环引用的类之后,我们就会立刻调用闭包中的代码。

苹果是这么解释 Unowned 引用

当封闭和捕获的实例将始终引用时,将闭包中的捕获定义为一个 Unowned ,并且这个变量将始终在同一时间被释放。

如果你知道你的引用将会清空,同时对这个变量的多次依赖彼此都是互相依赖。(其中的一个在另一个不存在的时候无法保留)那么你最好使用 Unowned 而不是 weak。因为你不会处理这些非必要的引用并将引用指针置空。

对于最简单的或者说最好的地方来使用 Unowned。是在闭包中通过懒定义就像下面的代码一样。

1
2
3
4
5
6
class Kraken {
let petName = "Krakey-poo"
lazy var businessCardName: () -> String = { [unowned self] in
return "Mr. Kraken AKA " + self.petName
}
}

在上面的代码中我们需要使用 Unowned 来阻止循环引用。Kraken 这个类在他的生命周期中持有了 businessCardName 这个成员变量的闭包,同时 businessCardName 在他的生命周期中也持有了 Kraken 的 self 这个变量。他们是彼此依赖的,所以他们就会同时被释放。因为这是最满足 Unowned 的规则。

然而,这样的情况下我们 不需要 考虑是否需要在这样的懒加载的情况中加入 unowned。

1
2
3
4
5
6
class Kraken {
let petName = "Krakey-poo"
lazy var businessCardName: String = {
return "Mr. Kraken AKA " + self.petName
}()
}

[unowned self] 在这里是不需要的,因为对于懒加载的变量。他不会被 retains。(译者注:因为前面的是闭包,而这个是在创建 String 的闭包中)该变量在第一次调用结束后,自己控制内存的分配和释放。(同时自己管理了自己的引用计数。)下图是我调用它的一个截图。这个截图来自评论区的 Алексей

String

总结

循环引用,在我们日常的代码过程中需要十分注意,并且要考虑到底要使用哪一种引用。其中内存的泄露和释放内存可以通过 weak 和 Unowned 来进行避免。我希望这篇文章对你的成长有一定的帮助。