iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 23
0
Software Development

Swift 菜鳥的30天系列 第 23

Day-23 Swift 語法(19) - 自動引用計數 (ARC)

自動引數計數 (Automatic Reference Counting)

Swift 使用自動引用技術(ARC)來追蹤跟管理你的APP的內存使用情況,多數情況下,這意味著 Swift 內存管理會一直運作,你不需要自己去考慮內存管理。當你的實例不再需要時,ARC會自動釋放實例使用的內存。


ARC的運作方式

每次你創建一個 class 的新實例時, ARC 都會分配一塊內存來儲存有關於這個實例的資訊。這個內存包含了實例類型的資訊,以及與該實例關聯的任何儲存屬性的值。

另外,當一個實例不在需要時,ARC 釋放該實例使用的內存,以便內存可以用在其他目的。這可以確保 class 的實例在不需要時不會佔用內存。

但是,如果 ARC 要釋放一個還在使用中的實例,將不在可能訪問實例屬性,或是調用實例的方法。事實上,如果,如果你嘗試訪問這個實例,你的 APP 很可能會崩潰。

為了確保這個實例在仍需要時不會消失, ARC 會追蹤當前引用每個 class 實例的屬性、常數和變數的數量。只要有至少一個對該實例引用仍存在,ARC就不會釋放該實例。

為了做到這一點,無論何時你將一個 class 實例賦給一個屬性、常數或是變數,該屬性、常數或是變數都會強制引用(strong)這個實例。之所以稱為”強引用“,因為它將這個實例保持住,只要這個強引用還在,就不允許它被重新分配。


ARC in Action

首先定義一個簡單的商品的class,並定義一個名為 name 儲存常數屬性,其中有一個初始化器,它設置了實例的 name 屬性並且輸出訊息的初始化器。 Person 類也有一個反初始化器,會在類的實例被銷毀的時候打印一條信息。:

class Product {
    let name:String
    init(name:String) {
        self.name = name
        print("\(name) 正在初始化...")
    }
    deinit {
        print("\(name) 正在反初始化...")
    }
}

我們下面的定義了三個 Product? 類型的變數,為新的 Product 實例設置多個引用。由於可選類型的變數會被自動初始化為一個 nil,目前還不會引用到該 class 的實例。

var product1:Product?
var product2:Product?
var product3:Product?

首先我們創建一個 Product 實例 product1,並賦值給他:

product1 = Product(name: "小熊餅乾")
// print 小熊餅乾 正在初始化...

結果:
https://ithelp.ithome.com.tw/upload/images/20180111/20107701NAEuGbNHod.png

因為 Product 實例已經賦值給了 product1 ,現在就有了一個從 product1 到該實例的強引用。因為至少有一個強引用,ARC 可以確保 Product 一直保持在內存中不被銷毀。如果你將同一個 Product 實例分配給了兩個變數,則 Product 實例又會多出兩個強引用:

product2 = product1 
product3 = product1

現在這個 Product 實例就有三個強引用,如果你透過賦給這些變數 nil 來斷開他的強引用,只留下一個強引用, Product 實例不會被釋放:

product1 = nil
product2 = nil

當你清楚表示不再使用這個實例時,也就是最後一個強引用被斷開時ARC會釋放它。

product3 = nil
// print 小熊餅乾 正在反初始化...

整段過程如下:
https://ithelp.ithome.com.tw/upload/images/20180111/20107701xkvVgtVJT5.png


Class 實例之間的循環強引用

在上面的例子中,ARC 能夠追踪你所創建的該實例的引用數量,會在實例強引用至少為一時保持著,並且會在實例不存在使用時銷毀。

總之,寫出某個類永遠不會變成零強引用是可能的。如果兩個 class 實例彼此擁有對方的強引用,因而每個實例都讓對方一直存在,就會發生這種循環的情況。這就是所謂的循環強引用。解決循環強引用問題,可以通過定義 class 之間的關係為弱引用(weak)或無主引用(unowned)來代替強引用。

我們用上面的 Product 的 class,在新增一個 Company 的 class。其中,我們在 Product 新增一個可選的初始化為 nil 的 company 屬性,也在 Company 中新增一個可選項 product ,初始值同樣為 nil:

// product
class Product {
    let name:String
    var company:Company?
    init(name:String) {
        self.name = name
        print("\(name) 正在初始化...")
    }
    deinit {
        print("\(name) 正在反初始化...")
    }
}

// company
class Company {
    let name:String
    var product:Product? 
    init(name:String) {
        self.name = name
        print("\(name) 正在初始化...")
    }
    deinit {
        print("\(name) 正在反初始化...")
    }
}

上面我們也各自定義了一個反初始化器,可以清楚的看見實例什麼時候被釋放,下面我們就如同前面的操作一樣,將它放到實例中,並賦值給他:

var product:Product?
var company:Company?

product = Product(name: "豆芽娃娃")
company = Company(name: "好想公司")

此時我們的變數 product 就有一個 Product 實例的強引用,變數 company則是來自 Company 實例的強引用,現在我們可以把兩格實例關聯在一起,一個加上商品名,一個則是加上公司名稱,我們使用感嘆號(!)來展開和訪問可選變數實例,並設置屬性:

product!.company = company
company!.product = product

這兩者互相會有循環強引用,Product 實例的有一個指向 Company 的強引用,Company 則有一個指向 Product 實例的強引用,所以當你我們 product 與 company 變數中的強引用,實例也不會被釋放:

product = nil
company = nil

因為他們有循環強引用的關係,所以不會輸出反初始化的訊息。

https://ithelp.ithome.com.tw/upload/images/20180111/20107701XSnYWsXAE7.png


解決實例之間的循環強引用

Swift 提供了兩種方法來處理強類型的屬性:弱引用(weak)和無主引用(unowned)。弱引用和無主引用允許循環引用中的一個實例引用另外一個實例而不保持強引用,使實例能夠互相引用而不產生循環強引用。

對於生命週期中會變為 nil 的實例使用弱引用。相反,對於初始化賦值後再也不會被賦值為 nil 的實例,使用無主引用。


弱引用

弱引用不會對其引用的實例保持強引用,因而不會阻止 ARC 釋放實例。這個特性阻止了引用變為循環強引用。聲明屬性或者變數時,在前面加上 weak 關鍵字表示這是一個弱引用。

由於弱引用不會強保持對實例的引用,所以說實例被釋放了弱引用仍舊引用著這個實例也是有可能的。因此,ARC會在被引用的實例被釋放是自動地設置弱引用為 nil 。由於弱引用需要允許它們的值為 nil ,它們一定得是可選類型。

下面的範例跟上面 Product 和 Company 的例子一致,但是有一個重要的區別。這次, Company 的 Product 屬性被聲明為弱引用(weak):

class Company {
    let name:String
    weak var product:Product? // 加上 weak
    init(name:String) {
        self.name = name
        print("\(name) 正在初始化...")
    }
    deinit {
        print("\(name) 正在反初始化...")
    }
}

其他部分都與上述相同的設置:

var product:Product?
var company:Company?

product = Product(name: "豆芽娃娃")
company = Company(name: "好想公司")

product!.company = company
company!.product = product

Product 實例依然保持對 Company 實例的強引用,但是 Company 實例現在對 Product 實例是弱引用。這意味著當你斷開 product 變數所保持的強引用時,再也沒有指向 product 實例的強引用了,由於再也沒有指向 product 實例的強引用,該實例會被釋放:

所以當我們設置 product = nil 時,product 實例被釋放:

https://ithelp.ithome.com.tw/upload/images/20180111/201077016c8PNGKlFM.png

這時我們在設置 company = nil ,因為變數 company 只剩對於 Company 實例的強引用,如果這時候斷開強引用,那麼這個實例也就被釋放:
https://ithelp.ithome.com.tw/upload/images/20180111/201077013AylUvHhQY.png


無主引用

和弱引用類似,無主引用不會保持住引用的實例。但不同於弱引用,無主引用假定是永遠有值。因此,無主引用總是被定義為非可選類型。你可以在聲明屬性或者變數時,在前面加上關鍵字 unowned 表示這是一個無主引用。

由於無主引用是非可選類型,可以直接訪問它。不過ARC無法在實例被釋放後將無主引用設為 nil ,因為非可選類型的變數不允許被賦值為 nil 。

我們一樣修改上述 Company 中的 class,將它的 product 設為無主引用,避免循環強引用:

class Company {
    let name:String
    unowned let product:Product
    init(name:String, productName: Product) {
        self.name = name
        self.product = productName
        print("\(name) 正在初始化...")
    }
    deinit {
        print("\(name) 正在反初始化...")
    }
}

下面我們一樣定義一個 Product 實例來測試:

var product:Product?

我們創建一個 Product 實例,用它初始化和分配一個新的 Company 實例作為 customer 中的 company 屬性:

product = Product(name: "豆芽娃娃")
product!.company = Company(name: "好想公司", productName: product!)

這時,現在 Product 實例對 Company 實例有一個強引用,並且 Company 實例對 Product 實例有一個無主引用。所以這時如果你斷開 product 變數的強引用時,那麼就再也沒有指向 Product 實例的強引用了,因為不再有 Product 的強引用,所以 Company 實例被釋放了。之後,因為也沒有指向 Company 實例的強引用,該實例也跟著釋放了,整體結果如下:

https://ithelp.ithome.com.tw/upload/images/20180111/20107701VGdIIxw9rM.png


閉包的循環強引用

循環強引用還會發生在當你將一個閉包賦值給 class 實例的某個屬性,並且這個閉包又使用了實例。這個閉包可能訪問了實例的某個屬性,或者閉包中調用了實例的某個方法。這兩種情況都導致了閉包 "捕獲" self,從而產生了循環強引用。

循環強引用的產生,是因為閉包和 class 相似,都是引用類型。當你把閉包賦值給了一個屬性,你實際上是把一個引用賦值給了這個閉包。實際上和上面的問題都是一樣的「兩個強引用讓彼此一直有效」。只是這次換成 class 實例和閉包互相引用。Swift 提供了一種方法來解決這個問題,稱之爲閉包補獲列表(closuer capture list)。

怕舉例錯誤只好跟著官方做一個 87% 像的

我們做一個英文成績的紀錄,其中我們 score 的值設為可選的 Int ,因為可能會有缺考,我們還宣告一個 testMsg 的變數,他的屬性的類型為 Void -> String ,大概就像是一個沒有參數,但要求你回傳一個 String 的函數,並在其中加入一個 if 判斷式,如果 score 中有值,就輸出他的名字和成績。如果沒有值,則返回他缺考的資訊:

class Engish {
    let name:String
    let score:Int?
    
    init(name:String, score:Int? = nil) {
        self.name = name
        self.score = score
    }
    lazy var testMsg: () -> String = {
        if let score = self.score{
            return "\(self.name) 英文成績為:\(score)"
        } else {
            return "\(self.name) 英文缺考"
        }
    }
    deinit {
        print("\(name)被反初始化了")
    }
    
}

可以像實例方法那樣去命名、使用我們宣告的 testMsg 屬性。總之,由於 testMsg 是閉包而不是實例方法,如果你想改變特定元素的 English 的話,可以用自定義的閉包來取代默認值。

https://ithelp.ithome.com.tw/upload/images/20180111/20107701PPJnuOFbmR.png

如果我們 score 中沒有值的話,則印出另外的資訊:
https://ithelp.ithome.com.tw/upload/images/20180111/20107701dkJyGt1zJ7.png

接下來我們用 English class來創建實例並打印出訊息:
https://ithelp.ithome.com.tw/upload/images/20180111/20107701gbeYICMds2.png

這時我們將這個實例設為 nil ,但是他並不會釋放這個實例,所以我們反初始化的訊息沒有被印出:
https://ithelp.ithome.com.tw/upload/images/20180111/20107701xiBsL649E4.png

實例的 testMsg 屬性持有閉包的強引用。但是,閉包在閉包內使用了 self(引用了 self.name 和self.score ),因此閉包捕獲了 self。這也表示閉包又反過來也有了 English 實例的強引用,就產生了循環強引用。

儘管閉包多次引用了 self ,它只捕獲 English 實例的一個強引用。


解決閉包的循環強引用

你可以通過定義捕獲列表作為閉包的定義來解決在閉包和 class 實例之間的循環強引用。捕獲列表定義了當在閉包內捕獲一個或多個引用類型的規則。正如在兩個 class 實例之間的循環強引用,宣告每個捕獲的引用為引用或無主引用而不是強引用。根據程式碼關係來決定使用弱引用還是無主引用。

Swift 會要求你在閉包中引用 self 成員時使用 self.someProperty 或 self.someMethod。提醒你可能會一不小心就捕獲了self 。


定義捕獲列表

捕獲列表中的每一項都由 weak 或 unowned 關鍵字與 class 實例的引用(像是 self )或初始化過的變數組成。這些項寫在方括號中用逗號分開。

把捕獲列表放在參數和返回類型前邊,如果它們存在的話:

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

如果閉包沒有指明形式參數列表或者返回類型,是因為它們會通過上下文推斷,那麼就把捕獲列表放在關鍵字 in 前邊,閉包最開始的地方:

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

弱引用和無主引用

在閉包和捕獲的實例總是互相引用並且總是同時釋放時,將閉包內的捕獲定義為無主引用。如果被捕獲的引用可能會變為 nil 時,定義一個弱引用的捕獲。弱引用總是可選項,當實例的引用釋放時會自動變為 nil 。這使我們可以在閉包體內檢查它們是否存在。如果被捕獲的引用永遠不會變為 nil ,應該用無主引用而不是弱引用。

因此我們加上捕獲列表 [ unowned self ] ,來表示我們是用無主引用而不是強引用來捕獲 self 。

class Engish {
    let name:String
    let score:Int?
    
    init(name:String, score:Int? = nil) {
        self.name = name
        self.score = score
    }
    lazy var testMsg: () -> String = {
        [unowned self] in //捕獲列表 ,定義為無主引用
        if let score = self.score{
            return "\(self.name) 英文成績為:\(score)"
        } else {
            return "\(self.name) 英文缺考"
        }
    }
    deinit {
        print("\(name)被反初始化了")
    }
    
}

這時候當我們實例的值為 nil時,這個實例就會被釋放,我們就能印出反初始化器的訊息:
https://ithelp.ithome.com.tw/upload/images/20180111/20107701iVSdxhTEmB.png


上一篇
Day-22 Swift 語法(18) - 反初始化 Deinitialization
下一篇
Day-24 Swift 語法(20) - 可選鏈 Optional Chaining
系列文
Swift 菜鳥的30天30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言