本文翻译自"WEAK, STRONG, UNOWNED, OH MY!" - A GUIDE TO REFERENCES IN SWIFT
作者:KrakenDev 译者:wuya
此文为原创翻译,已获原作者授权,版权归原作/译者所有,未经允许不得转载。
我们开始吧
ARC
ARC是苹果提供的自动内存管理机制,也就是平时我们说的自动引用计数。这意味着只有在一个对象的强引用为0的时候,才会释放它的内存。
STRONG
让我们从什么是强引用开始。它本质上也是一个普通引用,特殊的地方在于, 它会把引用对象的引用计数加1。任何对象只要还被一个强引用持有,它就不会被销毁。这个概念对下面解释的循环引用之类的问题非常重要.
在Swift,你到处都能看到强引用的身影,因为属性默认就是用Strong来修饰的。大体而言,在线性的继承关系中,使用Strong都是安全的。当强引用在继承关系中从父类传递到子类,使用Strong是完全没问题的
下面是一个使用Strong的例子.
class Kraken {
let tentacle = Tentacle() //strong reference to child.
}
class Tentacle {
let sucker = Sucker() //strong reference to child
}
在上面这个例子中,继承关系是线性的。 Kraken
有一个强引用指向了 Tentacle
,Tentacle
又有一个强引用指向了 Sucker
实例. 这个强引用的关系从父类 (Kraken
)开始一直指向到子类 (Sucker
).
这个和我们平常使用的动画方法很像:
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0.0
}
animateWithDuration
是 UIView
中的一个静态方法,在上面这个方法,UIView持有了block, block又持有了self。如我们上面所说,这种线性单向的强引用是没有问题的。
那如果一个子属性或者子类想要引用父类或者对象呢? 这就是我们要用到weak和unowned的时候了
WEAK 和 UNOWNED (弱引用)
WEAK
weak引用仅仅是一个指针,指向了被引用的对象,它不会保护对象不被ARC销毁。因为strong 引用会使得对象的引用计数加1,但weak引用不会。当对象被销毁了,weak引用会指向0。这一点保证了当你去访问一个weak对象时,它一定是一个有效的对象,或者是nil。
在Swift,所有的weak引用都必须是一个可变的optional对象。因为当这个被引用的对象没有任何Strong引用时,它会被置为nil。
下面是一个错误的例子,编译时会报错:
class Kraken {
//let is a constant! All weak variables MUST be mutable.
weak let tentacle = Tentacle()
}
因为 tentacle
是一个 let
常量。 Let
定义为不可改变的常量,因为weak变量可能会在没有其他strong引用时变成nil,所以Swift编译器会要求你把weak变量声明为var
。
weak变量最重要的作用就是用来避免可能出现的计数循环引用问题。计数循环引用就是两个对象互相都有strong引用指向对方,这时候ARC无法生成销毁这两个对象的代码,下面是这个苹果文档中的图片清晰地说明了这种情况:
这是一个使用了NSNotification
API的例子,很好的说明了循环引用的情况,让我们来看看相关代码:
class Kraken {
var notificationObserver: ((Notification) -> Void)?
init() {
notificationObserver = NotificationCenter.default.addObserver(forName: "humanEnteredKrakensLair", object: nil, queue: .main) { notification in
self.eatHuman()
}
}
deinit {
NotificationCenter.default.removeObserver(notificationObserver)
}
}
在这个例子中,我们就出现了一个循环引用。我们都知道,Swift里的闭包和Objective-C的block很相似。如果一个变量在闭包的外面被定义,当这个变量在闭包里被使用的时候,闭包会自动生成一个strong引用指向该变量。
在这里,NotificationCenter
持有了一个闭包,而这个闭包在调用了 eatHuman()
时,会自动捕获self并对self强引用。在调用deinit
前,我们不会销毁这个闭包,但deinit
永远不会被ARC调用因为闭包对Kraken实例有一个Strong引用。
我们在使用 NSTimers
和 NSThread
的时候同样可能出现类似的问题。
解决这个问题的方法就是使用weak来修饰闭包内捕获的变量,这样就能打破计数引用循环。这时,我们的引用关系图就像这样:
把self引用改为weak,self的引用计数不会加1,这样,ARC就能在需要的时候正确的销毁self。
在闭包中使用 weak
和 unowned
的语法是使用中括号[],例子:
let closure = { [weak self] in
self?.doSomething() //Remember, all weak variables are Optionals!
}
为什么要用中括号[]呢?这看起来太奇怪了!在Swift中,[]会让我们想起数组Arrays
。没错,你可以在闭包中使用多个变量,就像这样:
//Look at that sweet, sweet Array of capture values.
let closure = { [weak self, unowned krakenInstance] in
self?.doSomething() //weak variables are Optionals!
krakenInstance.eatMoreHumans() //unowned variables are not.
}
是不是看起来就像一个数组? 所以,你现在能理解为什么要用中括号了吧。现在,我们可以通过给闭包中捕获的变量加上[weak self] 来解决notification例子中的循环引用问题:
NotificationCenter.default.addObserver(forName: "humanEnteredKrakensLair", object: nil, queue: .main) { [weak self] notification in //The retain cycle is fixed by using capture lists!
self?.eatHuman() //self is now an optional!
}
另一种需要用到weak 和 unowned的情况是在各个类之间使用委托的时候,因为类都是引用语义。在Swift里,struct和enum同样可以使用委托,但它们都是值语义,并不会造成循环引用。下面是个例子:
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
对它的delegate
属性是一个强引用。
同时
Kraken
的tentacle
属性中,对Tentacle
是一个强引用。
既然如此,那我们在声明delegate的时候给它加个weak吧
weak var delegate: LossOfLimbDelegate?
怎么回事?这样编译报错?!这是因为protocols不是一个类,它不能被weak修饰。
这时,我们只能给把protocol继承class
protocol LossOfLimbDelegate: class { //The protocol now inherits class
func limbHasBeenLost()
}
那什么时候不需要用 :class
,我们来看看苹果的文档:
When do we not use :class
? Well according to Apple:
“Use a class-only protocol when the behavior defined by that protocol’s requirements assumes or requires that a conforming type has reference semantics rather than value semantics.”
是这样的,像上面的例子一样,当你要用到引用关系时,你就要加上 :class
。如果你用的是struct或者enum来使用委托,就不需要用:class
,因为struct或者enum是值语义,class是引用语义。
UNOWNED
weak和unowned引用在表现上非常相似,但又不完全一样。unowned和weak一样,在使用的时候,都不会增加对象的引用计数。在Swift里,被unowned修饰的意味着它不是一个optional对象,这使得它比optional对象更好管理(这里,和 Implicitly Unwrapped Optionals
又不一样)。而且,unowned引用的对象在被销毁时,不会被自动置0,这意味着在使用unowned对象时,可能会遇到野指针。有点类似与Objective-C中的unsafe_unretained引用。
这里你可能会疑惑,既然weak和unowned都不会增加引用计数,可以用来避免循环计数引用,那我们怎么区分来使用它们?我们来看看苹果的文档
:
“Use a weak reference whenever it is valid for that reference to become nil at some point during its lifetime. Conversely, use an unowned reference when you know that the reference will never be nil once it has been set during initialization.”
好了,那我们知道了,如果你能确定这个引用对象一旦被初始化之后就永远不会变成nil,那就用unowned;不是的话,就用weak
这里是一个循环引用的例子,并且闭包里捕获的self不会被置为nil:
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时强引用了它,而闭包又是self的一个强引用属性。我们只需要简单的在闭包加上 [unowned self]
就可以打破循环引用:
closure = { [unowned self] in
self.string = "Hello, World!"
}
在这个例子,我们可以认为self不会是nil,因为我们在类初始化后马上调用了这个闭包。
苹果的文档中是这样解释 unowned references的:
“Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.”
如果你知道,在程序某个地方你的引用对象将会被正确的置0,而且你的两个引用对象相互依赖,那么你应该用unowned而不是weak,因为这样你可以不需要在程序中过多的考虑这些引用对象是不是在什么时候被系统置为0了。
有个非常适合使用unowned的场景,就是把它用在一个懒加载属性中的闭包里:
class Kraken {
let petName = "Krakey-poo"
lazy var businessCardName: (Void) -> String = { [unowned self] in
return "Mr. Kraken AKA " + self.petName
}
}
我需要在这里使用unowned self 来防止循环计数引用。在Kraken
的生命周期里都它都持有着 businessCardName
闭包,而businessCardName
闭包在它的生命周期里也一直持有着 Kraken
(self)。它们之间相互依赖,所以将会在需要的时候同时销毁,符合使用unowned的条件。
但是,可不要被这样的懒加载属性迷惑了,它可不是一个闭包:
class Kraken {
let petName = "Krakey-poo"
lazy var businessCardName: String = {
return "Mr. Kraken AKA " + self.petName
}()
}
在上面这个例子里,并不需要用到Unowned self
,因为这里的闭包并没有真正持有什么东西。这个成员变量只是单纯的把闭包返回的值赋给自身,然后在这个变量第一次使用后就马上把闭包销毁了(这时self的计数减1)。这个输出截图证实了这一点:
总结
引用计数循环是个让人头疼的东西。但只要我们在编码时多加小心,并正确的使用weak 和 unowned,存泄漏,野指针之类的问题都是可以避免的。希望这个教程能对你有所帮助!