昨天介紹了 ARC 的管理方式以及如何執行,還有在怎麼樣的情形之下會發生強引用循環,今天主要就要來介紹如何解決強引用循環所造成的記憶體洩漏。
當我們使用類型別的屬性時,Swift 提供了兩種解決強引用循環的方法:弱引用 (weak references) 和無主引用 (unowned references)。
弱引用和無主引用能夠使引用循環中的一個實例引用另一個實例而不保持強引用,則實例可以相互引用而不會創造強引用循環。
當另一個實例的生命週期更短時,使用弱引用,即可以先釋放另一個實例。在上述的 Apartment
範例中,適合公寓在其生命週期中的某個時刻沒有租戶,因此弱引用是在這種情況下打破引用循環的適當方式。相反的,當另一個實例具有相同的生命週期或更長的生命週期時,請使用無主引用。
弱引用是一種對它引用的實例不會強制保留,因此不會阻止 ARC 處理引用的實例。此行為會阻止引用成為強引用循環的一部分。在屬性或變數宣告之前的位置用 weak
關鍵字來指出為弱引用。
因為弱引用不會對它所引用的實例做強制保留,所以在弱引用仍然引用它的情況下,可以釋放該實例。因此當引用它的實例被釋放時,ARC 會自動將弱引用設置為 nil
。並且,因為弱引用需要允許它們的值在運行時更改為 nil
,所以它們總是被宣告為可選型別的變數而不是常數。
我們可以檢查弱引用中是否存在值,就像任何其他可選值一樣,並且我們永遠不會引用不再存在的無效實例。
註:當 ARC 設置弱引用為
nil
時,不會調用屬性觀察者。
下面的範例與上面的 Person
和 Apartment
範例相同,但有一個重要區別。這一次,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") }
}
來自兩個變數(john
和 unit4A
)的強引用以及兩個實例之間的連結如前所述:
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
關鍵字來指示無主引用。
一個無主引用應該總是有一個值。因此,ARC 永遠不會將無主引用的值設置為 nil
,這代表應使用非可選類型定義無主引用。
僅當您確定引用始終引用不會被釋放的實例時,才使用無主引用。
如果在釋放該實例後嘗試訪問無主引用的值,則會出現運行錯誤。
以下範例定義兩個類 Customer
和 CreditCard
,這是將銀行的客戶或可能為該客戶提供信用卡作為模型,這兩個類分別都將另一個類的實例存儲為屬性,這種關係有可能創造一個強引用循環。
Customer
和 CreditCard
之間的關係與上述弱參考範例中看到的 Apartment
和 Person
之間的關係略有不同。在此數據模型中,客戶可能擁有或不擁有信用卡,但信用卡始終與客戶相關聯。 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 還提供不安全的無主引用 -- 例如:基於效能原因。與所有不安全的操作一樣,我們負責檢查該代碼的安全性。
我們通過編寫無主(不安全)來表示不安全的無主引用。如果在釋放引用的實例後嘗試訪問不安全的無主引用,則程序將嘗試訪問實例所在的內存位置,這是一種不安全的操作。
上面的弱和無主引用的例子涵蓋了兩個更常見的場景,其中有必要打破一個強引用循環。
Person
和 Apartment
範例顯示了兩個屬性(均允許為 nil
)可能導致強引用循環的情況,使用弱引用是此方案最好的解決方法。
Customer
和 CreditCard
範例顯示了一種情況,即允許一個屬性為 nil
,另一個不能為 nil
的屬性可能導致強引用循環,使用無主引用是此方案最好的解決方法。
但是,還有第三種情況,其中兩個屬性始終有值,並且一旦初始化完成,這兩個屬性都不應為 nil
。在這種情況下,將一個類上的無主屬性與另一個類上的隱式解包可選屬性組合起來很有用。
這使得一旦初始化完成就可以直接訪問兩個屬性(無需可選的解包),同時仍然避免引用循環。本節介紹如何設置此類關係。
下面的範例定義了兩個類,Country
和 City
,每個類都將另一個類的實例存儲為屬性。在這個數據模型中,每個國家都必須始終擁有一個首都,每個城市必須始終屬於一個國家。為了表示這一點,類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
實例已完全初始化。這代表著只要設置了屬性 name
,Country
初始值設定項就可以開始引用並傳遞隱式 self
屬性。因此,當 Country
初始值設定項設置自己的 capitalCity
屬性時,Country
初始化程序可以將 self
作為 City
初始值設定項的參數之一。
所有這些代表著我們可以在單一語句中建立 Country
和 City
實例,而無需創造強引用循環,並且可以直接訪問 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
屬性,同時仍然避免強引用循環。