iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 14
0
自我挑戰組

iOS 新手開發的大小事系列 第 14

Day 14: 自動引用計數 (Automatic Reference Counting) -2

  • 分享至 

  • xImage
  •  

前情提要

昨天介紹了 ARC 的管理方式以及如何執行,還有在怎麼樣的情形之下會發生強引用循環,今天主要就要來介紹如何解決強引用循環所造成的記憶體洩漏。


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

當我們使用類型別的屬性時,Swift 提供了兩種解決強引用循環的方法:弱引用 (weak references) 和無主引用 (unowned references)。

弱引用和無主引用能夠使引用循環中的一個實例引用另一個實例而不保持強引用,則實例可以相互引用而不會創造強引用循環。

當另一個實例的生命週期更短時,使用弱引用,即可以先釋放另一個實例。在上述的 Apartment 範例中,適合公寓在其生命週期中的某個時刻沒有租戶,因此弱引用是在這種情況下打破引用循環的適當方式。相反的,當另一個實例具有相同的生命週期或更長的生命週期時,請使用無主引用。

弱引用 (Weak References)

弱引用是一種對它引用的實例不會強制保留,因此不會阻止 ARC 處理引用的實例。此行為會阻止引用成為強引用循環的一部分。在屬性或變數宣告之前的位置用 weak 關鍵字來指出為弱引用。

因為弱引用不會對它所引用的實例做強制保留,所以在弱引用仍然引用它的情況下,可以釋放該實例。因此當引用它的實例被釋放時,ARC 會自動將弱引用設置為 nil。並且,因為弱引用需要允許它們的值在運行時更改為 nil,所以它們總是被宣告為可選型別的變數而不是常數。

我們可以檢查弱引用中是否存在值,就像任何其他可選值一樣,並且我們永遠不會引用不再存在的無效實例。

註:當 ARC 設置弱引用為 nil 時,不會調用屬性觀察者。

下面的範例與上面的 PersonApartment 範例相同,但有一個重要區別。這一次,Apartment 型別的租戶屬性被宣告為弱參考:

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

來自兩個變數(johnunit4A)的強引用以及兩個實例之間的連結如前所述:

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

現在,我們將這兩個實例連結在一起的參考文件如下:

Person 實例仍然具有對 Apartment 實例的強引用,但Apartment 實例現在具有對 Person 實例的弱引用。這代表著當你通過將 john 變數設置為 nil 而破壞 john 變數所持有的強引用時,就不再對 Person 實例的強引用:

john = nil
// Prints "John Appleseed is being deinitialized"

因為沒有更多對 Person 實例的強引用,所以它被釋放並且tenant 屬性設置為 nil

unit4A = nil
// Prints "Apartment 4A is being deinitialized"

因為沒有更多對 Apartment 實例的強引用,所以它也會被釋放:

在使用垃圾收集的系統中,弱指標有時用於實現簡單的緩存機制,因為只有當內存壓力觸發垃圾收集時才會釋放沒有強引用的對象。但是,使用 ARC 時,一旦刪除了最後一個強引用,就會釋放值,使得弱引用不適用於此類目的。

無主引用 (Unowned References)

就像一個弱引用一樣,無主引用並不能對它所引用的實例有強大保持。但是與弱引用不同,當另一個實例具有相同的生命週期或更長的生命週期時,將使用無主引用。在屬性或變量宣告之前加入 unowned 關鍵字來指示無主引用。

一個無主引用應該總是有一個值。因此,ARC 永遠不會將無主引用的值設置為 nil,這代表應使用非可選類型定義無主引用。

僅當您確定引用始終引用不會被釋放的實例時,才使用無主引用。
如果在釋放該實例後嘗試訪問無主引用的值,則會出現運行錯誤。

以下範例定義兩個類 CustomerCreditCard,這是將銀行的客戶或可能為該客戶提供信用卡作為模型,這兩個類分別都將另一個類的實例存儲為屬性,這種關係有可能創造一個強引用循環。

CustomerCreditCard 之間的關係與上述弱參考範例中看到的 ApartmentPerson 之間的關係略有不同。在此數據模型中,客戶可能擁有或不擁有信用卡,但信用卡始終與客戶相關聯。 CreditCard 實例永遠不會超過它所引用的客戶。為了表示這一點,Customer 類具有屬性 card 的可選型別,但 CreditCard 類具有無主(且非可選型別)的屬性 customer

此外,只能通過將 number 值和 customer 實例傳遞給自定義 CreditCard 初始化程序來創建新的 CreditCard 實例。這可確保在創建 CreditCard 實例時,CreditCard 實例始終具有與之關聯的客戶實例。

由於信用卡將始終擁有客戶,因此我們將其屬性 customer 定義為無主引用,以避免強引用循環:

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized") }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}

CreditCard 類的屬性 number 是使用 UInt64 而不是 Int 定義的,以確保屬性 number 的容量足以在 32 位和 64 位系統上存儲 16 位卡號。

接著我們宣告一個 Customer 可選型別的變數,並給入初始值,使其產生一個新實例,再將此實例內的屬性 card 指定為一個 CreditCard 新實例:

var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

現在我們已經鏈接了兩個實例,下圖是引用的情形:

Customer 實例現在具有對 CreditCard 實例的強引用,並且 CreditCard 實例具有對 Customer 實例的無主引用。

由於無主客戶引用,當我們破壞 john 變數持有的強引用時,不再有對 Customer 實例的強引用:

由於沒有對 Customer 實例的更強引用,因此將其釋放。發生這種情況後,沒有更多對 CreditCard 實例的強引用,它也被釋放:

john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"

上面的最後一行代碼顯示,在 john 變量設置為 nil 之後,Customer 實例和 CreditCard 實例的反初始化器都印出了它們的「反初始化」訊息。

上面的範例顯示如何使用安全的無主引用。對於需要禁用運行時安全檢查的情況,Swift 還提供不安全的無主引用 -- 例如:基於效能原因。與所有不安全的操作一樣,我們負責檢查該代碼的安全性。

我們通過編寫無主(不安全)來表示不安全的無主引用。如果在釋放引用的實例後嘗試訪問不安全的無主引用,則程序將嘗試訪問實例所在的內存位置,這是一種不安全的操作。

無主引用和隱式解包的可選屬性 (Unowned References and Implicitly Unwrapped Optional Properties)

上面的弱和無主引用的例子涵蓋了兩個更常見的場景,其中有必要打破一個強引用循環。

PersonApartment 範例顯示了兩個屬性(均允許為 nil)可能導致強引用循環的情況,使用弱引用是此方案最好的解決方法。

CustomerCreditCard 範例顯示了一種情況,即允許一個屬性為 nil,另一個不能為 nil 的屬性可能導致強引用循環,使用無主引用是此方案最好的解決方法。

但是,還有第三種情況,其中兩個屬性始終有值,並且一旦初始化完成,這兩個屬性都不應為 nil。在這種情況下,將一個類上的無主屬性與另一個類上的隱式解包可選屬性組合起來很有用。

這使得一旦初始化完成就可以直接訪問兩個屬性(無需可選的解包),同時仍然避免引用循環。本節介紹如何設置此類關係。

下面的範例定義了兩個類,CountryCity,每個類都將另一個類的實例存儲為屬性。在這個數據模型中,每個國家都必須始終擁有一個首都,每個城市必須始終屬於一個國家。為了表示這一點,類Country 具有屬性 capitalCity ,類 City 具有屬性 country

class Country {
    let name: String
    var capitalCity: City!
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

要設置兩個類之間的相互依賴性,City 的初始化程序將採用 Country 實例,並將此實例存儲在其 country 屬性中。

City 的初始化程序在 Country 的初始化程序中調用。但是,Country 的初始化程序無法將 self 傳遞給 City 的初始化,直到完全初始化新的 Country 實例。

要處理此要求,請將 Country 的屬性 capitalCity 宣告為隱式解包的可選屬性,即由驚嘆號結尾 City!。這代表著屬性 capitalCity 的預設值為 nil,與任何其他可選類別一樣,但可以在不需要打開其值的情況下訪問。

由於 capitalCity 具有 nil 的預設值,因此只要 Country 實例在其初始值設定項中設置屬性 name ,就會認為新的 Country 實例已完全初始化。這代表著只要設置了屬性 nameCountry 初始值設定項就可以開始引用並傳遞隱式 self 屬性。因此,當 Country 初始值設定項設置自己的 capitalCity 屬性時,Country 初始化程序可以將 self 作為 City 初始值設定項的參數之一。

所有這些代表著我們可以在單一語句中建立 CountryCity 實例,而無需創造強引用循環,並且可以直接訪問 capitalCity 屬性,而無需使用驚嘆號來解包其可選值:

var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"

在上面的範例中,使用隱式展開的可選項意味著滿足所有兩階段類初始化程序要求。初始化完成後,可以像使用非可選值一樣使用和訪問 capitalCity 屬性,同時仍然避免強引用循環。


上一篇
Day 13: 自動引用計數 (Automatic Reference Counting) -1
下一篇
Day 15: 自動引用計數 (Automatic Reference Counting) -3
系列文
iOS 新手開發的大小事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言