Swift學習筆記16——自動引用計數(Automatic Reference Counting)

       對於類實例,它可能存在被多個變量引用的情況。如果在還有變量引用的情況下釋放了改實例的話,那麼其他變量再嘗試訪問這個實例的方法或屬性的時候,程序就會崩潰。所以必須確保在以後都沒有變量使用這個實例的情況下,才能去釋放這個實例。對於值類型(結構體等),因爲不存在多個變量對應一個實例的情況,所以不會有上述問題。爲了解決這個問題,Swift使用自動引用計數(ARC)來管理內存。它只對引用類型起作用,對於值類型不起作用。

解釋引用計數的概念

當你給一個創建一個類實例,並且把這個類實例賦值給某個變量或常量的時候,那麼這個變量或常量就“擁有”這個實例,我們稱爲有了一個“強引用”。所謂的引用計數,就是這個實例被多少個常量或變量強引用了。

class Apple {
    deinit{
        print("deinit")
    }
}
var a:Apple! = Apple()

上面這最後一句代碼就是變量a對一個Apple類的實例有了一個強引用。這時候這個實例的引用計數爲1。

因爲類實例是引用類型,所以你可以把這個引用傳遞給其他的常量或變量。

var b = a
let v = a

這時候這個實例就被三個變量或常量所引用,這個時候它的引用計數爲3.

當你把其中的b設爲nil的時候,引用計數就會變爲2.

       當該實例的引用計數變爲0的時候,這個實例就會被銷燬,銷燬的時候就會調用它的deinit方法。注:因爲上面用了常量,所以不能手動把常量設爲nil,只能等這個常量離開作用域後被系統自動銷燬。(如果你是在main.swift的全局中定義這些變量或常量的話,因爲在main執行完之後纔會釋放這些變量或常量,所以不會打印deinit。但是你可以把他們放到一個函數裏面,然後在main.swift裏面調用這個函數。當這個函數執行完之後,這些變量或常量就會被釋放。)

所以自動引用計數的規則很簡單:

1、賦值給不加修飾符的常量和變量的時候,實例的引用計數加1。

2、當一個變量設爲nil,或者變量(常量)離開作用域的時候,這個常量或變量所引用實例的引用計數減1。

3、當一個實例的引用計數爲0的時候,它就會被銷燬。


但是上面看似簡單的規則也會有很多問題。比如下面的循環引用問題。

先定義兩個類,一個Telephone類和一個Person類。Telephone類有一個Person類的屬性,Person類裏面有一個Telephone類的屬性。

class Telephone {
    var person: Person?
    deinit{
        print("Telephone deinit")
    }
}

class Person {
    var telephone: Telephone?
    deinit{
        print("Person deinit")
    }
}

然後我們定義一個Person類的實例和一個Telephone類的實例。並對他們賦值

var person = Person()
var telephone = Telephone()
person.telephone = telephone
telephone.person = person

下面分析一下Person實例和Telephone實例的引用計數。

在定義Person實例的時候,賦值給了變量person,所以第一句代碼後,Person實例的引用計數爲1。同樣第二句代碼後,Telephone實例的引用計數也爲1。

然後第三句代碼把telephone賦值給了person.telephone。也就是person.telephone也對這個Telephone實例有了強引用,這時候Telephone實例的引用計數爲2。

同樣,第四局過後,Person實例的引用計數也爲2。

現在的狀態是一個Person實例強引用了一個Telephone實例,這個Telephone實例又強引用了這個Person實例。你強引用我,我強引用你。這樣就成了一個循環引用。

接着執行下面代碼,person變量和telephone變量釋放對實例的強引用。

 person = nil
 telephone = nil
但是Person實例中的telephone屬性仍然強引用着Telephone實例。同樣的Telephone實例的person屬性也引用着Person實例。所以Person實例和Telephone實例的引用計數都爲1。但此時我們已經沒辦法再訪問Person和Telephone的實例了。同時又因爲他們的引用計數都爲1,系統也不會釋放他們。這樣就造成了內存泄露。


爲了解決這種循環引用的問題,辦法就是截斷這個循環。

第一種笨笨的解決方法就是在你把person或telephone變量設爲nil之前,把person.telephone或telephone.person設爲nil。這樣就手動切斷了循環引用。

而通用的解決方法就是引用一個新概念——弱引用(Weak Reference)

弱引用和強引用最大區別就是:當你把一個實例賦值給一個弱引用變量的時候,這個變量的引用計數不會加1。

爲了實現這一點,在定義變量的時候在最前面加上weak關鍵字。下面我們重新定義Person類和Telephone類。

class Telephone {
    weak var person: Person?   //把這個變量定義爲了一個弱引用變量
    deinit{
        print("Telephone deinit")
    }
}

class Person {
    var telephone: Telephone?
    deinit{
        print("Person deinit")
    }
}

然後我們再次調用下面的代碼

var person: Person? = Person()
var telephone: Telephone? = Telephone()
person!.telephone = telephone
telephone!.person = person     //第4句
person = nil                 // 執行完這句後打印  Person deinit
telephone = nil              // 執行完這句後打印  Telephone deinit

因爲Telephone類裏面的person屬性是弱引用的,所以執行完了第4句之後,Telephone實例被telephone變量和person.telephone實例所引用,引用計數爲2。而Person實例只被person變量所引用,引用計數爲1.

當執行完person = nil 之後,person的引用計數就變爲了0, 這個時候系統就會釋放Person實例,這個過程中,person.telephone也會被釋放,所以會導致Telephone實例的引用計數減1,變爲1。

當執行完telephone = nil 之後,Telephone實例的引用計數變爲0。系統釋放Telephone實例。


關於這個弱引用再補充幾點

第一、當一個弱引用變量所引用的實例被釋放的時候,這個弱引用變量會被自動置爲nil。

第二、因爲第一條的內容,所以弱引用只能對變量使用,並且必須是可選類型。

第三、如果你在創建實例的時候就把它複製給一個弱引用變量,因爲弱引用變量不會增加這個實例的引用計數,所以這個實例創建後立馬就會被銷燬。

第四、如果你將一個已經賦值的弱引用變量賦值給一個強引用變量(常量),那麼這個實例的引用計數會加1。


Unowned Reference

Unowned Reference和弱引用一樣,不會對實例產生強引用。區別在於Unowned Reference假設它所指向的實例總是有值的。所以Unowned Reference一般不會設置爲可選類型。但缺點就是當Unowned Reference所指向的實例被釋放的時候,Unowned Reference變量不會自動置爲nil。

語法就是將weak關鍵字替換爲unowned。但一個變量永遠不會爲nil的時候,建議使用unowned修飾。


循環引用第二種情況——閉包循環引用

       在閉包的時候我們說過,閉包是引用類型,且會捕獲值。設想,你把一個閉包聲明爲一個類的屬性的時候,這個類的實例擁有了對這個閉包的強引用。此時如果你在這個閉包裏面訪問了這個類的其他屬性(self.someProperty)或者方法(self.someMethod)的話。那麼這個閉包就會捕獲所訪問的屬性或方法,統稱"捕獲了self"。在訪問實例的屬性或方法的時候,必須使用self.的方式。Swift此意在提醒你可能會產生循環引用。

那麼這時候又是一個循環引用了,self引用閉包,閉包引用self。導致這個實例永遠不會被釋放。

下面定義一個有閉包的Person類

class Person {
    var name: String?
    lazy var printName: Void->Void = {
        print(self.name)
    }
    init(name: String){
        self.name = name;
    }
    deinit{
        print("Person deinit")
    }
}

這個閉包我們聲明爲了lazy類型,因爲如果你想要在閉包裏面訪問到self的話,必須是在類初始化之後纔行。而一般的屬性是在類初始化的最開頭階段初始化的,所以不加lazy的閉包不能訪問self關鍵字。上面的代碼很明顯閉包和類實例已經可能會產生循環引用了。爲什麼說可能呢?因爲如果你一直沒用到閉包的話,那麼這個閉包就不會被初始化,所以也不會產生閉包對self的強引用,也就談不上循環引用了。

所以如果僅僅執行下面代碼

var p: Person? = Person(name: "Kate")
p = nil
//打印出 Person deinit  

但是如果執行下面代碼

var p: Person? = Person(name: "Kate")
p?.printName()
p = nil
//打印出 Optional("Kate")

這時候因爲循環引用導致Person實例不會被釋放。


解決這個循環引用同樣有兩種方式。

第一種是在不需要這個實例的時候,將這個可能會引起循環引用的閉包設爲nil。

第二種是利用閉包的捕獲列表。

下面是第二種方法的介紹

下面是語法定義例子,分別是有參數和沒參數的閉包。在這種情況下,閉包對捕獲的self不會產生強引用。(題外話,在OC中是通過定義另外一個對self的弱引用變量,然後將這個弱引用變量傳遞給block來實現的。)

    //有參數的情況
    lazy var printName: ((String)->Void)? = {
        [unowned self] (say: String) -> Void in
        print(say,self.name)
    }
    //沒參數的情況
    lazy var printName2: (Void->Void)? = {
        [weak self] in
        print(self!.name)
    }


這裏就是用兩個關鍵字weak和unowned將self修飾。weak和unowned的區別和之前所講的是一樣的。


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