Swift內存管理(ARC)之循環引用

Swift中是採用自動引用計數(ARC,AutomaticReferenceCounting)機制來對內存進行管理的。

一、簡述ARC如何工作:

每當你每創建一個新的對象,它便會分配一塊內存來存儲該對象的相關信息。當你不再需要這個對象的時候,它就會自動釋放這個對象,避免它再佔用內存空間。當然,如果該對象只要至少存在一個引用就不會被釋放。另外,你如果訪問了一個已經被釋放的對象,則很有可能會出現崩潰(野指針)。
拿《The Swift Programming Language》裏面的例子來套一下:

//創建一個Cat類,自帶常量name屬性
class Cat {
    let name: String
    init(name: String) {
    //初始化name屬性
        self.name = name;
        print("\(name) 對象已經被初始化")
    }

    deinit {
        print("\(name) 對象釋放成功!")
    }
}

接下來定義三個變量類型爲:Cat?用來對一個新的Cat對象建立多個引用。因爲變量是一個可選類型(是Cat?而不是Cat), 他們會自動給變量初始化爲nil,所有當前它不需要引用一個Cat對象。

var cat1: Cat?
var cat2: Cat?
var cat3: Cat?

創建一個新的Cat對象,並對之前定義的三個變量進行賦值操作:

cat1 = Cat(name: "jack")
//輸出:jack 對象已經被初始化

當你調用Cat類構造器,它會輸出:”jack 對象已經被初始化”。這就代表已經完成了對象初始化操作。接下來,一個新的Cat對象被賦值給了 cat1變量,從而建立了cat1對新Cat實例對象的強引用。因此,它存在了至少一個強引用。這個新的Cat對象就不會被釋放。
如果你把同一個Cat實例對象賦值給另外兩個變量,它則又多建立了兩個強引用。

//通過賦值對Cat實例對象建立強引用
cat2 = cat1
cat3 = cat1

此時這個Cat實例對象已經有三個強引用了。
如果你用把其中兩個變量賦值爲nil的方式來打破這些強引用,對象是不會被釋放的。因爲它還存在一個強引用。

//通過把置nil來打破cat1,cat2對Cat實例對象的強引用
cat1 = nil
cat2 = nil

只有直到對象所有的引用被打破,ARC纔會釋放該對象

//打破最後一個強引用
cat3 = nil
//輸出:jack 對象釋放成功!

當最後一個強引用被打破後,這個Cat對象被成功釋放,輸出:jack 對象釋放成功!

二、循環引用

1、什麼是循環引用?
簡單的來說就是兩個對象互相持有對方,使對方處於活躍狀態不被釋放,從而導致了循環引用。套個例子:

//創建一個Cat類,擁有一個String類型的name屬性與一個可選類型的dog屬性。
class Cat {
    let name: String
    init(name: String) {
        self.name = name;
        print("\(name) 對象已經被初始化")
    }
    var dog: Dog?

    deinit {
        print("\(name) 對象釋放成功!")
    }
}

//創建一個Dog類,擁有一個String類型的type屬性與一個可選類型的jack屬性。
class Dog {
    let type: String
    init(type: String) {
        self.type = type
        print("\(type) 對象已經被初始化")
    }

    var cat: Cat?
    deinit {
        print("\(type) 對象釋放成功")
    }
}

定義兩個變量並初始化

var jack: Cat?
var tom: Dog?

jack = Cat(name: "jack")
//print:jack 對象已經被初始化
tom = Dog(type: "red")
//print:red 對象已經被初始化

建立強引用

jack!.dog = tom
tom!.cat = jack

通過把對象置nil打破強引用

jack = nil
tom = nil

這個地方Cat對象和Dog對象都不會被釋放,因爲他們兩個之間還存在強引用。

  • 解決循環引用辦法
    Swift提供了兩種方法來解決循環引用問題:

    (1)弱引用(weak)

    (2)無主引用(unowned)

聲明變量或屬性時,在前面加上”weak”關鍵詞表示它是一個弱引用。如果確定訪問屬性或函數的時候該對象不會被釋放,也可以用無主引用(unowned)來取代。unowned跟Objective-C中的unsafe_unretained類似。它與weak的區別在於weak在引用的對象被釋放後,會自動置nil。所以它必須聲明成可選類型(?)。而unowned在引用對象被釋放後不會自動置nil(因爲非可選類型的變量不能被設置成nil),而是保留對原有對象的無效引用,一旦對象被釋放,再訪問該對象的屬性或函數就會造成崩潰。官方建議是當你在可以確定訪問屬性或函數所屬對象不會被釋放時,使用unowned。否則,還是使用weak吧!

解決上面代碼的循環引用問題,可做如下修改:

class Dog {
    let type: String
    init(type: String) {
        self.type = type
        print("\(type) 對象已經被初始化")
    }

    weak var jack: Cat
    deinit {
        print("\(type) 對象釋放成功")
    }
}
jack = nil
tom = nil

2、常見的循環引用問題:

(1)delegate
在項目中我們經常會用到代理模式,在Objective-C中我們一般在delegate前面加上weak關鍵詞,表示它是一個弱引用。從而達到打破循環引用的效果。

//在前面加上@objc是指定這裏的代碼屬於Objective-C的代碼,因爲Swift中協議可以用於class、struct、enum。對於基本數據類型,它不是對象,沒有引用計數這個概念,因此無法使用weak關鍵詞。  Objective-C中的協議只適用於類,所以這裏需要加上@objc。
@objc protocol AClassDelegate {
    func wash()
}

class A {
    weak var delegate: AClassDelegate!
    func cry() {

    }
    func play() {
        delegate.wash()
    }
}

class B: AClassDelegate {

    func feed() {
        let nurse = A()
        nurse.delegate = self
        nurse.cry()
    }

    @objc func wash() {
        print("begin wash and sleep")
    }
}

(2)Block(閉包)中的循環引用
Block(閉包)的循環引用在於它會持有所有引用的元素。比如我們在Block(閉包)中訪問了self,那麼它就持有了self。這樣一來就形成了一個環:self持有Block(閉包),Block(閉包)持有self。下面我們舉個栗子加以說明:類A中有個閉包,我們在閉包中訪問了self的name屬性。

class A {
    let name: String
    lazy var wash: Void ->Void = {
        var name = self.name + " nurse"
        print("wash room is \(name)")
    }

    init(name: String) {
        self.name = name
        print("\(self.name) 創建了")
    }

    deinit {
        print("\(self.name) 釋放了")
    }
}

class B {
    func play() {
        var nurse: A?
        nurse = A(name: "jack")
        nurse?.wash()
    }
}

var baby: B?
baby = B()
baby?.play()

輸出結果:
jack 創建了
wash room is jack nurse

因爲wash是self的一個屬性,所以wash被self持有。這個時候我們又在閉包中訪問了self的name屬性,所以它又在閉包中持有了self。這樣便導致了循環引用。爲了解決閉包中的循環引用問題,Swift中提供了一個叫”閉包捕獲列表”的解決方案。

  • 定義一個捕獲列表

    捕獲列表中的每一項都是一個用weak或 unowned關鍵字與一個實例對象的引用(如self)或者是一個用初始化值的變量(如delegate = self.delegate!)搭配成對,配對之間用逗號隔開,最後用方括號括起來。如下所示:

    
    lazy var someClosure:(Int, String) -> String = {
        [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
    
    }
    

    如果閉包中沒有指定參數列表或者返回類型,可直接把捕獲列表放置在閉包開始的地方(花括號後面)然後緊跟着是”in”關鍵詞,示例代碼如下:

    lazy var someClosure:Void -> String = {
        [unowned self, weak delegate = self.delegate!]  in
    
    }

    如果捕獲的引用總是不會爲nil,那麼它應該作爲一個無主引用(unowned)被捕獲,這樣會比弱引用(weak)要好。

  • 解決Block(閉包)中的循環引用,ClassA中的Block(閉包)代碼可做以下修改:

lazy var wash:Void ->Void = { [weak self] in
//這個地方使用self!.name而不是self.name。是因爲weak修飾,self有可能爲nil,如果是unowned修飾,這個地方應該是self.name。因爲unowned修飾的self永遠不可能爲空。
        var name = self!.name + " nurse"
        print("wash room is \(name)")
    }

 輸出結果:
jack 創建了
wash room is jack nurse
jack 釋放了

參考資料及文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章