"weak,strong,unowned"--Swift使用指南

本文翻譯自"WEAK, STRONG, UNOWNED, OH MY!" - A GUIDE TO REFERENCES IN SWIFT
作者:KrakenDev 譯者:wuya
此文為原創(chuàng)翻譯,已獲原作者授權(quán),版權(quán)歸原作/譯者所有,未經(jīng)允許不得轉(zhuǎn)載。

我們開始吧

ARC

ARC是蘋果提供的自動(dòng)內(nèi)存管理機(jī)制,也就是平時(shí)我們說的自動(dòng)引用計(jì)數(shù)。這意味著只有在一個(gè)對象的強(qiáng)引用為0的時(shí)候,才會(huì)釋放它的內(nèi)存。

STRONG

讓我們從什么是強(qiáng)引用開始。它本質(zhì)上也是一個(gè)普通引用,特殊的地方在于, 它會(huì)把引用對象的引用計(jì)數(shù)加1。任何對象只要還被一個(gè)強(qiáng)引用持有,它就不會(huì)被銷毀。這個(gè)概念對下面解釋的循環(huán)引用之類的問題非常重要.

在Swift,你到處都能看到強(qiáng)引用的身影,因?yàn)閷傩阅J(rèn)就是用Strong來修飾的。大體而言,在線性的繼承關(guān)系中,使用Strong都是安全的。當(dāng)強(qiáng)引用在繼承關(guān)系中從父類傳遞到子類,使用Strong是完全沒問題的

下面是一個(gè)使用Strong的例子.

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

在上面這個(gè)例子中,繼承關(guān)系是線性的。 Kraken 有一個(gè)強(qiáng)引用指向了 Tentacle ,Tentacle 又有一個(gè)強(qiáng)引用指向了 Sucker 實(shí)例. 這個(gè)強(qiáng)引用的關(guān)系從父類 (Kraken)開始一直指向到子類 (Sucker).

這個(gè)和我們平常使用的動(dòng)畫方法很像:

UIView.animate(withDuration: 0.3) {
    self.view.alpha = 0.0
}

animateWithDurationUIView中的一個(gè)靜態(tài)方法,在上面這個(gè)方法,UIView持有了block, block又持有了self。如我們上面所說,這種線性單向的強(qiáng)引用是沒有問題的。

那如果一個(gè)子屬性或者子類想要引用父類或者對象呢? 這就是我們要用到weak和unowned的時(shí)候了

WEAK 和 UNOWNED (弱引用)

WEAK

weak引用僅僅是一個(gè)指針,指向了被引用的對象,它不會(huì)保護(hù)對象不被ARC銷毀。因?yàn)閟trong 引用會(huì)使得對象的引用計(jì)數(shù)加1,但weak引用不會(huì)。當(dāng)對象被銷毀了,weak引用會(huì)指向0。這一點(diǎn)保證了當(dāng)你去訪問一個(gè)weak對象時(shí),它一定是一個(gè)有效的對象,或者是nil。

在Swift,所有的weak引用都必須是一個(gè)可變的optional對象。因?yàn)楫?dāng)這個(gè)被引用的對象沒有任何Strong引用時(shí),它會(huì)被置為nil。

下面是一個(gè)錯(cuò)誤的例子,編譯時(shí)會(huì)報(bào)錯(cuò):

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

因?yàn)?tentacle是一個(gè) let 常量。 Let 定義為不可改變的常量,因?yàn)閣eak變量可能會(huì)在沒有其他strong引用時(shí)變成nil,所以Swift編譯器會(huì)要求你把weak變量聲明為var。

weak變量最重要的作用就是用來避免可能出現(xiàn)的計(jì)數(shù)循環(huán)引用問題。計(jì)數(shù)循環(huán)引用就是兩個(gè)對象互相都有strong引用指向?qū)Ψ?,這時(shí)候ARC無法生成銷毀這兩個(gè)對象的代碼,下面是這個(gè)蘋果文檔中的圖片清晰地說明了這種情況:

BA759544-E58E-4F6F-BE36-20EA92DBCCB5.png

這是一個(gè)使用了NSNotification API的例子,很好的說明了循環(huán)引用的情況,讓我們來看看相關(guān)代碼:

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)
    }
}

在這個(gè)例子中,我們就出現(xiàn)了一個(gè)循環(huán)引用。我們都知道,Swift里的閉包和Objective-C的block很相似。如果一個(gè)變量在閉包的外面被定義,當(dāng)這個(gè)變量在閉包里被使用的時(shí)候,閉包會(huì)自動(dòng)生成一個(gè)strong引用指向該變量。

在這里,NotificationCenter 持有了一個(gè)閉包,而這個(gè)閉包在調(diào)用了 eatHuman()時(shí),會(huì)自動(dòng)捕獲self并對self強(qiáng)引用。在調(diào)用deinit前,我們不會(huì)銷毀這個(gè)閉包,但deinit 永遠(yuǎn)不會(huì)被ARC調(diào)用因?yàn)殚]包對Kraken實(shí)例有一個(gè)Strong引用。

我們在使用 NSTimersNSThread的時(shí)候同樣可能出現(xiàn)類似的問題。

解決這個(gè)問題的方法就是使用weak來修飾閉包內(nèi)捕獲的變量,這樣就能打破計(jì)數(shù)引用循環(huán)。這時(shí),我們的引用關(guān)系圖就像這樣:

retain-cycle-broken.png

把self引用改為weak,self的引用計(jì)數(shù)不會(huì)加1,這樣,ARC就能在需要的時(shí)候正確的銷毀self。

在閉包中使用 weakunowned 的語法是使用中括號(hào)[],例子:

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

為什么要用中括號(hào)[]呢?這看起來太奇怪了!在Swift中,[]會(huì)讓我們想起數(shù)組Arrays。沒錯(cuò),你可以在閉包中使用多個(gè)變量,就像這樣:

//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.
}

是不是看起來就像一個(gè)數(shù)組? 所以,你現(xiàn)在能理解為什么要用中括號(hào)了吧?,F(xiàn)在,我們可以通過給閉包中捕獲的變量加上[weak self] 來解決notification例子中的循環(huán)引用問題:

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的情況是在各個(gè)類之間使用委托的時(shí)候,因?yàn)轭惗际且谜Z義。在Swift里,struct和enum同樣可以使用委托,但它們都是值語義,并不會(huì)造成循環(huán)引用。下面是個(gè)例子:

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屬性是一個(gè)強(qiáng)引用。

同時(shí)

Krakententacle 屬性中,對Tentacle 是一個(gè)強(qiáng)引用。

既然如此,那我們在聲明delegate的時(shí)候給它加個(gè)weak吧

weak var delegate: LossOfLimbDelegate?

怎么回事?這樣編譯報(bào)錯(cuò)?!這是因?yàn)閜rotocols不是一個(gè)類,它不能被weak修飾。

這時(shí),我們只能給把protocol繼承class

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

那什么時(shí)候不需要用 :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.”

是這樣的,像上面的例子一樣,當(dāng)你要用到引用關(guān)系時(shí),你就要加上 :class。如果你用的是struct或者enum來使用委托,就不需要用:class,因?yàn)閟truct或者enum是值語義,class是引用語義。

UNOWNED

weak和unowned引用在表現(xiàn)上非常相似,但又不完全一樣。unowned和weak一樣,在使用的時(shí)候,都不會(huì)增加對象的引用計(jì)數(shù)。在Swift里,被unowned修飾的意味著它不是一個(gè)optional對象,這使得它比optional對象更好管理(這里,和 Implicitly Unwrapped Optionals
又不一樣)。而且,unowned引用的對象在被銷毀時(shí),不會(huì)被自動(dòng)置0,這意味著在使用unowned對象時(shí),可能會(huì)遇到野指針。有點(diǎn)類似與Objective-C中的unsafe_unretained引用。

這里你可能會(huì)疑惑,既然weak和unowned都不會(huì)增加引用計(jì)數(shù),可以用來避免循環(huán)計(jì)數(shù)引用,那我們怎么區(qū)分來使用它們?我們來看看蘋果的文檔

“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.”

好了,那我們知道了,如果你能確定這個(gè)引用對象一旦被初始化之后就永遠(yuǎn)不會(huì)變成nil,那就用unowned;不是的話,就用weak

這里是一個(gè)循環(huán)引用的例子,并且閉包里捕獲的self不會(huì)被置為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.

在這個(gè)例子,循環(huán)引用是因?yàn)殚]包在捕獲self時(shí)強(qiáng)引用了它,而閉包又是self的一個(gè)強(qiáng)引用屬性。我們只需要簡單的在閉包加上 [unowned self] 就可以打破循環(huán)引用:

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

在這個(gè)例子,我們可以認(rèn)為self不會(huì)是nil,因?yàn)槲覀冊陬惓跏蓟篑R上調(diào)用了這個(gè)閉包。
蘋果的文檔中是這樣解釋 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.”

如果你知道,在程序某個(gè)地方你的引用對象將會(huì)被正確的置0,而且你的兩個(gè)引用對象相互依賴,那么你應(yīng)該用unowned而不是weak,因?yàn)檫@樣你可以不需要在程序中過多的考慮這些引用對象是不是在什么時(shí)候被系統(tǒng)置為0了。

有個(gè)非常適合使用unowned的場景,就是把它用在一個(gè)懶加載屬性中的閉包里:

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

我需要在這里使用unowned self 來防止循環(huán)計(jì)數(shù)引用。在Kraken 的生命周期里都它都持有著 businessCardName閉包,而businessCardName 閉包在它的生命周期里也一直持有著 Kraken (self)。它們之間相互依賴,所以將會(huì)在需要的時(shí)候同時(shí)銷毀,符合使用unowned的條件。

但是,可不要被這樣的懶加載屬性迷惑了,它可不是一個(gè)閉包:

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

在上面這個(gè)例子里,并不需要用到Unowned self,因?yàn)檫@里的閉包并沒有真正持有什么東西。這個(gè)成員變量只是單純的把閉包返回的值賦給自身,然后在這個(gè)變量第一次使用后就馬上把閉包銷毀了(這時(shí)self的計(jì)數(shù)減1)。這個(gè)輸出截圖證實(shí)了這一點(diǎn):

image

總結(jié)

引用計(jì)數(shù)循環(huán)是個(gè)讓人頭疼的東西。但只要我們在編碼時(shí)多加小心,并正確的使用weak 和 unowned,存泄漏,野指針之類的問題都是可以避免的。希望這個(gè)教程能對你有所幫助!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容