IOS

弱引用?强引用?未持有?额滴神啊– Swift 引用计数指导

http://www.cocoachina.com/swift/20160202/15182.html

 

作者:Hector Matos

原文:“WEAK, STRONG, UNOWNED, OH MY!” – A GUIDE TO REFERENCES IN SWIFT


我做梦都在担心我的代码里会出现引用循环(Retain Cycle)。我相信这是我们大家普遍担心的问题。你我虽素未蒙面,但时常隐约听闻:“什么时候该用weak? ‘unowned’又是什么鬼?!”我们发现之所以会存在这样的问题在于我们知道在的swift代码中 使用strong,weak和 unowned 说明符可以用来避免循环引用,但是我们不知道具体该使用哪一个。别急,有老夫呢。我希望这篇知道文章能够帮助你在代码中正确的使用它们。

我们开始吧!

ARC

ARC 苹果版本的自动内存管理的编译时间特性。它代表了自动引用计数(Automatic Reference Counting)。也就是对于一个对象来说,只有在引用计数为0的情况下内存才会被释放。

Strong(强引用)

让我们从什么是强引用说起。它实质上就是普通的引用(指针等等),但是它的特殊之处在于它能够通过使对象的引用计数+1来保护对象,避免引用对象被ARC机制销毁。本质上来讲,任何对象只要有强引用,它就不会被销毁掉。记住这点对我接下来要讲的引用循环等其他知识来说很重要。

强引用在swift中无处不在。事实上,当你声明一个属性时,它就默认是一个强引用。一般来说,当对象之间的关系为线性时,使用强引用是安全的。当对象之间的强引用是从父层级流向子层级时,用强引用通常也是ok的。

下面是一些强引用的例子

1
2
3
4
5
6
7
8
9
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)。

同样的,在动画blocks中,引用关系也类似:

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

由于animateWithDuration是UIView的一个静态方法,这里的闭包作为父层级,self作为子层级。

那么当一个子层级想引用父层级会怎么样呢?这里我们就要用到 weak 和 unowned 引用了。

Weak 和 unowned 引用

weak

weak 引用并不能保护所引用的对象被ARC机制销毁。强引用能使被引用对象的引用计数+1,而弱引用不会。此外,若弱引用的对象被销毁后,弱引用的指针会被清空。这样保证了当你调用一个弱引用对象时,你能得到一个对象或者nil.

在swift中,所有的弱引用都是非常量的可选类型(对比 var 和 let) ,因为当没有强引用对象引用的的时候,弱引用对象能够并且会变成nil。

例如,这样的代码不会通过编译

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

因为tentacle是一个let常量。Let常量在运行的时候不能被改变。因为弱引用变量在没有被强引用的条件下会变成nil,所以Swift 编译器要求你必须用var来定义弱引用对象。

值得注意的地方是,使用弱引用变量能够避免你出现可能的引用循环。当两个对象相互强引用的时候会出现一个引用循环。如果2个对象相互引用对方,ARC就不能给这两个对象发出合适的释放信息,因为这两个对象彼此相互依存。下图是从苹果官方简洁的图片,它很好的解释了这种情况:

1454052936680325

一个比较恰当的例子就是通知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)
        }
    }
}

在这种情况下我们有一个引用循环。你会发现,Swift中的闭包的表现类似与Objective-C的blocks。如果在闭包范围之外声明变量,那么在闭包中使用这个变量时,会对该变量产生另一个强引用。唯一的例外是使用值类型的变量,比如Swift中的 Ints、Strings、Arrays以及Dictionaries等。

在这里,当你调用eatHuman( ) 时,NSNotificationCenter就保留了一个闭包以强引用方式捕获self。经验告诉我们,你应该在deinit方法中清除通知监听对象。这段代码的问题在于我们没有清除掉block直到deinit.但是deinit 永远都不会被ARC机制调用,因为闭包对Kraken实例有强引用。

另外在NSTimers和NSThread也可能会出现这种情况。

解决这种情况的方法就是在闭包的捕获列表中使用对self的弱引用。这样就能够打破强引用循环。那么,我们的对象引用图就会像这样:

1454052989898014

把self变成weak不会让self 的引用计数+1,因此ARC机制就能在合适的时间释放掉对象。

想要在闭包使用 weak 和 unowned 变量,你应该用[]把它们括起来。如:

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

在上面的代码中,为什么要把 weak self 要放在方括号内?看上去简直秀逗了!在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!
}

其他我们用到weak和unowned变量的情况是当你使用协议在多个类之间实现代理时,因为Swift中类使用的是reference semantics。在Swift中,结构体和枚举同样能够遵循协议,但是它们用的是value semantics。如果像这样一个父类带上一个子类使用委托使用了代理:

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属性中对Tentacle也有一个强引用。我们通过在代理声明前面加上weak来解决这个问题:

1
weak var delegate: LossOfLimbDelegate?

是不是发现这样写不能通过编译?不能通过编译的原因是非 class类型的协议不能被标识成weak。这里,我们必须让协议继承:class,从而使用一个类协议将代理属性标记为weak。

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

我们什么时候用 :class,通过苹果官方文档

“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。在结构体和枚举的情况下,没有必要用:class,因为结构体和枚举是value semantics,而类是 reference semantics.

UNOWNED 

weak引用和unowned引用有些类似但不完全相同。Unowned 引用,像weak引用一样,不会增加对象的引用计数。然而,在Swift里,一个unowned引用有着非可选类型的优点。这样相比于借助和使用optional binding更易于管理。这和隐式可选类型(Implicity Unwarpped Optionals)区别不大。此外,unowned引用是non-zeroing(非零的) ,这表示着当一个对象被销毁时,它指引的对象不会清零。也就是说使用unowned引用在某些情况下可能导致 dangling pointers(野指针url)。你是不是跟我一样想起了用Objective -C的时候, unowned引用映射到了 unsafe_unretained引用。 http://www.krakendev.io/when-to-use-implicitly-unwrapped-optionals/

看到这里是不是有点蛋疼了。既然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,那么就用weak引用。反之,当你知道引用对象在初始化后永远都不会为nil就用unowned.

现在你就知道了:就像是implicitly unwrapped optional(隐式可选类型),如果你能保证在使用过程中引用对象不会为nil,用unowned 。如果不能,那么就用weak

下面就是个很好的例子。Class 里面的闭包捕获了self,self永远不会为nil。

1
2
3
4
5
6
7
8
9
10
11
12
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] 就能打破引用循环:

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

在这个例子中,由于我们在初始化RetainCycle类后立即调用了闭包,所以我们可以认为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.”

如果你知道你引用的对象会在正确的时机释放掉,且它们是相互依存的,而你不想写一些多余的代码来清空你的引用指针,那么你就应该使用unowned引用而不是weak引用。

像下面这种懒加载在闭包中使用self就是一个使用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 self 来避免引用循环。Kraken 和 businessCardName在它们的生命周期内都相互持有对方。它们相互持有,因此总是被同时销毁,满足使用unowned 的条件。

然而,不要把下面的懒加载变量与闭包混淆:

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

在懒加载变量中调用closure时,由于没有retain closure,所以不需要加 unowned self。变量只是简单的把闭包的结果assign 给了自己,闭包在使用后就被立即销毁了。下面的截图很好的证明了这点。(截图是厚着脸皮评论区Алексей的拷贝)

1454053485567250

总结 

引用循环很坑爹!但是谨慎码代码,理清你的引用逻辑,通过使用weak 和 unowned 能够很好的避免内存循环和 内存泄露。我希望这篇指导手册能够帮到你们。

Happy coding fellow nerds.

Leave a Reply

Your email address will not be published. Required fields are marked *