1. 程式人生 > >Swift 引用計數總結 Strong,Weak, unowned 簡單使用

Swift 引用計數總結 Strong,Weak, unowned 簡單使用

每天一小結,必須讀幾篇部落格在閒暇時,下面開始進入正題:

ARC

ARC 蘋果版本的自動記憶體管理的編譯時間特性。它代表了自動引用計數(Automatic Reference Counting)。也就是對於一個物件來說,只有在引用計數為0的情況下記憶體才會被釋放。

Strong(強引用)

讓我們從什麼是強引用說起。它實質上就是普通的引用(指標等等),但是它的特殊之處在於它能夠通過使物件的引用計數+1來保護物件,避免引用物件被ARC機制銷燬。本質上來講,任何物件只要有強引用,它就不會被銷燬掉。記住這點對我接下來要講的引用迴圈等其他知識來說很重要。

強引用在swift中無處不在。事實上,當你宣告一個屬性時,它就預設是一個強引用。一般來說,當物件之間的關係為線性時,使用強引用是安全的。當物件之間的強引用是從父層級流向子層級時,用強引用通常也是ok的。

下面是一些強引用的例子

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中,引用關係也類似:

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。 

例如,這樣的程式碼不會通過編譯 

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

因為tentacle是一個let常量。Let常量在執行的時候不能被改變。因為弱引用變數在沒有被強引用的條件下會變成nil,所以Swift 編譯器要求你必須用var來定義弱引用物件。 

值得注意的地方是,使用弱引用變數能夠避免你出現可能的引用迴圈。當兩個物件相互強引用的時候會出現一個引用迴圈。如果2個物件相互引用對方,ARC就不能給這兩個物件發出合適的釋放資訊,因為這兩個物件彼此相互依存。下圖是從蘋果官方簡潔的圖片,它很好的解釋了這種情況: 

44.png

一個比較恰當的例子就是通知APIs,看一下下面的程式碼: 

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的弱引用。這樣就能夠打破強引用迴圈。那麼,我們的物件引用圖就會像這樣: 

26.png

把self變成weak不會讓self 的引用計數+1,因此ARC機制就能在合適的時間釋放掉物件。 

想要在閉包使用 weak 和 unowned 變數,你應該用[]把它們括起來。如:

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

在上面的程式碼中,為什麼要把 weak self 要放在方括號內?看上去簡直秀逗了!在Swift中,我們看到方括號就會想到陣列。你猜怎麼著?你可以在在閉包內定義多個捕獲值!例如: 

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],我們就可以解決之前那段有引用迴圈的通知程式碼。 

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。如果像這樣一個父類帶上一個子類使用委託使用了代理: 

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來解決這個問題: 

weak var delegate: LossOfLimbDelegate?

是不是發現這樣寫不能通過編譯?不能通過編譯的原因是非 class型別的協議不能被標識成weak。這裡,我們必須讓協議繼承:class,從而使用一個類協議將代理屬性標記為weak。 

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。 

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!"
}

在這個例子中,由於我們在初始化RetainCycle類後立即呼叫了閉包,所以我們可以認為self永遠不會為nil。

“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的很好例子: 

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

我們需要用unowned self 來避免引用迴圈。Kraken 和 businessCardName在它們的生命週期內都相互持有對方。它們相互持有,因此總是被同時銷燬,滿足使用unowned 的條件。 

然而,不要把下面的懶載入變數與閉包混淆: 

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

在懶載入變數中呼叫closure時,由於沒有retain closure,所以不需要加 unowned self。變數只是簡單的把閉包的結果assign 給了自己,閉包在使用後就被立即銷燬了。下面的截圖很好的證明了這點。(截圖是厚著臉皮評論區Алексей的拷貝) 

下載.jpg